bmad-method 6.2.2 → 6.2.3-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +78 -0
- package/package.json +8 -8
- package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
- package/src/core-skills/bmad-init/scripts/bmad_init.py +35 -4
- package/src/core-skills/bmad-init/scripts/tests/test_bmad_init.py +64 -0
- package/tools/{cli → installer}/bmad-cli.js +3 -1
- package/tools/{cli/lib → installer}/cli-utils.js +3 -4
- package/tools/{cli → installer}/commands/install.js +3 -3
- package/tools/{cli → installer}/commands/status.js +4 -4
- package/tools/{cli → installer}/commands/uninstall.js +5 -5
- package/tools/installer/core/config.js +52 -0
- package/tools/{cli/installers/lib → installer}/core/custom-module-cache.js +1 -1
- package/tools/installer/core/existing-install.js +127 -0
- package/tools/installer/core/install-paths.js +129 -0
- package/tools/installer/core/installer.js +1790 -0
- package/tools/{cli/installers/lib → installer}/core/manifest-generator.js +3 -3
- package/tools/{cli/installers/lib → installer}/core/manifest.js +2 -2
- package/tools/{cli/installers/lib/custom/handler.js → installer/custom-handler.js} +1 -1
- package/tools/{cli/installers/lib → installer}/ide/_config-driven.js +30 -397
- package/tools/{cli/installers/lib → installer}/ide/manager.js +1 -53
- package/tools/installer/ide/platform-codes.js +37 -0
- package/tools/installer/ide/platform-codes.yaml +190 -0
- package/tools/{cli/installers/lib → installer}/ide/shared/module-injections.js +1 -1
- package/tools/{cli/installers/lib → installer}/message-loader.js +2 -2
- package/tools/installer/modules/custom-modules.js +197 -0
- package/tools/installer/modules/external-manager.js +323 -0
- package/tools/{cli/installers/lib/core/config-collector.js → installer/modules/official-modules.js} +714 -43
- package/tools/{cli/lib → installer}/ui.js +65 -299
- package/tools/javascript-conventions.md +5 -0
- package/tools/bmad-npx-wrapper.js +0 -38
- package/tools/cli/installers/lib/core/dependency-resolver.js +0 -743
- package/tools/cli/installers/lib/core/detector.js +0 -223
- package/tools/cli/installers/lib/core/ide-config-manager.js +0 -157
- package/tools/cli/installers/lib/core/installer.js +0 -3002
- package/tools/cli/installers/lib/ide/_base-ide.js +0 -657
- package/tools/cli/installers/lib/ide/platform-codes.js +0 -100
- package/tools/cli/installers/lib/ide/platform-codes.yaml +0 -341
- package/tools/cli/installers/lib/modules/external-manager.js +0 -136
- package/tools/cli/installers/lib/modules/manager.js +0 -928
- package/tools/cli/lib/config.js +0 -213
- package/tools/cli/lib/platform-codes.js +0 -116
- package/tools/lib/xml-utils.js +0 -13
- /package/tools/{cli → installer}/README.md +0 -0
- /package/tools/{cli → installer}/external-official-modules.yaml +0 -0
- /package/tools/{cli/lib → installer}/file-ops.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/agent-command-generator.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/bmad-artifacts.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/path-utils.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/skill-manifest.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/agent-command-template.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/antigravity.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-agent.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-task.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-tool.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow-yaml.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow-yaml.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/rovodev.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/trae.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/windsurf-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/split/.gitkeep +0 -0
- /package/tools/{cli/installers → installer}/install-messages.yaml +0 -0
- /package/tools/{cli/lib → installer}/project-root.js +0 -0
- /package/tools/{cli/lib → installer}/prompts.js +0 -0
- /package/tools/{cli/lib → installer}/yaml-format.js +0 -0
package/tools/{cli/installers/lib/core/config-collector.js → installer/modules/official-modules.js}
RENAMED
|
@@ -1,30 +1,701 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const yaml = require('yaml');
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
const prompts = require('../prompts');
|
|
5
|
+
const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');
|
|
6
|
+
const { CLIUtils } = require('../cli-utils');
|
|
7
|
+
const { ExternalModuleManager } = require('./external-manager');
|
|
8
|
+
|
|
9
|
+
class OfficialModules {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.externalModuleManager = new ExternalModuleManager();
|
|
12
|
+
// Config collection state (merged from ConfigCollector)
|
|
10
13
|
this.collectedConfig = {};
|
|
11
|
-
this.
|
|
14
|
+
this._existingConfig = null;
|
|
12
15
|
this.currentProjectDir = null;
|
|
13
|
-
this._moduleManagerInstance = null;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
+
* Module configurations collected during install.
|
|
20
|
+
*/
|
|
21
|
+
get moduleConfigs() {
|
|
22
|
+
return this.collectedConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Existing module configurations read from a previous installation.
|
|
27
|
+
*/
|
|
28
|
+
get existingConfig() {
|
|
29
|
+
return this._existingConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build a configured OfficialModules instance from install config.
|
|
34
|
+
* @param {Object} config - Clean install config (from Config.build)
|
|
35
|
+
* @param {Object} paths - InstallPaths instance
|
|
36
|
+
* @returns {OfficialModules}
|
|
37
|
+
*/
|
|
38
|
+
static async build(config, paths) {
|
|
39
|
+
const instance = new OfficialModules();
|
|
40
|
+
|
|
41
|
+
// Pre-collected by UI or quickUpdate — store and load existing for path-change detection
|
|
42
|
+
if (config.moduleConfigs) {
|
|
43
|
+
instance.collectedConfig = config.moduleConfigs;
|
|
44
|
+
await instance.loadExistingConfig(paths.projectRoot);
|
|
45
|
+
return instance;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Headless collection (--yes flag from CLI without UI, tests)
|
|
49
|
+
if (config.hasCoreConfig()) {
|
|
50
|
+
instance.collectedConfig.core = config.coreConfig;
|
|
51
|
+
instance.allAnswers = {};
|
|
52
|
+
for (const [key, value] of Object.entries(config.coreConfig)) {
|
|
53
|
+
instance.allAnswers[`core_${key}`] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const toCollect = config.hasCoreConfig() ? config.modules.filter((m) => m !== 'core') : [...config.modules];
|
|
58
|
+
|
|
59
|
+
await instance.collectAllConfigurations(toCollect, paths.projectRoot, {
|
|
60
|
+
skipPrompts: config.skipPrompts,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return instance;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Copy a file to the target location
|
|
68
|
+
* @param {string} sourcePath - Source file path
|
|
69
|
+
* @param {string} targetPath - Target file path
|
|
70
|
+
* @param {boolean} overwrite - Whether to overwrite existing files (default: true)
|
|
71
|
+
*/
|
|
72
|
+
async copyFile(sourcePath, targetPath, overwrite = true) {
|
|
73
|
+
await fs.copy(sourcePath, targetPath, { overwrite });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Copy a directory recursively
|
|
78
|
+
* @param {string} sourceDir - Source directory path
|
|
79
|
+
* @param {string} targetDir - Target directory path
|
|
80
|
+
* @param {boolean} overwrite - Whether to overwrite existing files (default: true)
|
|
81
|
+
*/
|
|
82
|
+
async copyDirectory(sourceDir, targetDir, overwrite = true) {
|
|
83
|
+
await fs.ensureDir(targetDir);
|
|
84
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
88
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
89
|
+
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
await this.copyDirectory(sourcePath, targetPath, overwrite);
|
|
92
|
+
} else {
|
|
93
|
+
await this.copyFile(sourcePath, targetPath, overwrite);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* List all available built-in modules (core and bmm).
|
|
100
|
+
* All other modules come from external-official-modules.yaml
|
|
101
|
+
* @returns {Object} Object with modules array and customModules array
|
|
102
|
+
*/
|
|
103
|
+
async listAvailable() {
|
|
104
|
+
const modules = [];
|
|
105
|
+
const customModules = [];
|
|
106
|
+
|
|
107
|
+
// Add built-in core module (directly under src/core-skills)
|
|
108
|
+
const corePath = getSourcePath('core-skills');
|
|
109
|
+
if (await fs.pathExists(corePath)) {
|
|
110
|
+
const coreInfo = await this.getModuleInfo(corePath, 'core', 'src/core-skills');
|
|
111
|
+
if (coreInfo) {
|
|
112
|
+
modules.push(coreInfo);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add built-in bmm module (directly under src/bmm-skills)
|
|
117
|
+
const bmmPath = getSourcePath('bmm-skills');
|
|
118
|
+
if (await fs.pathExists(bmmPath)) {
|
|
119
|
+
const bmmInfo = await this.getModuleInfo(bmmPath, 'bmm', 'src/bmm-skills');
|
|
120
|
+
if (bmmInfo) {
|
|
121
|
+
modules.push(bmmInfo);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { modules, customModules };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get module information from a module path
|
|
130
|
+
* @param {string} modulePath - Path to the module directory
|
|
131
|
+
* @param {string} defaultName - Default name for the module
|
|
132
|
+
* @param {string} sourceDescription - Description of where the module was found
|
|
133
|
+
* @returns {Object|null} Module info or null if not a valid module
|
|
134
|
+
*/
|
|
135
|
+
async getModuleInfo(modulePath, defaultName, sourceDescription) {
|
|
136
|
+
// Check for module structure (module.yaml OR custom.yaml)
|
|
137
|
+
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
|
138
|
+
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
|
|
139
|
+
let configPath = null;
|
|
140
|
+
|
|
141
|
+
if (await fs.pathExists(moduleConfigPath)) {
|
|
142
|
+
configPath = moduleConfigPath;
|
|
143
|
+
} else if (await fs.pathExists(rootCustomConfigPath)) {
|
|
144
|
+
configPath = rootCustomConfigPath;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Skip if this doesn't look like a module
|
|
148
|
+
if (!configPath) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Mark as custom if it's using custom.yaml OR if it's outside src/bmm or src/core
|
|
153
|
+
const isCustomSource =
|
|
154
|
+
sourceDescription !== 'src/bmm-skills' && sourceDescription !== 'src/core-skills' && sourceDescription !== 'src/modules';
|
|
155
|
+
const moduleInfo = {
|
|
156
|
+
id: defaultName,
|
|
157
|
+
path: modulePath,
|
|
158
|
+
name: defaultName
|
|
159
|
+
.split('-')
|
|
160
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
161
|
+
.join(' '),
|
|
162
|
+
description: 'BMAD Module',
|
|
163
|
+
version: '5.0.0',
|
|
164
|
+
source: sourceDescription,
|
|
165
|
+
isCustom: configPath === rootCustomConfigPath || isCustomSource,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Read module config for metadata
|
|
169
|
+
try {
|
|
170
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
171
|
+
const config = yaml.parse(configContent);
|
|
172
|
+
|
|
173
|
+
// Use the code property as the id if available
|
|
174
|
+
if (config.code) {
|
|
175
|
+
moduleInfo.id = config.code;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
moduleInfo.name = config.name || moduleInfo.name;
|
|
179
|
+
moduleInfo.description = config.description || moduleInfo.description;
|
|
180
|
+
moduleInfo.version = config.version || moduleInfo.version;
|
|
181
|
+
moduleInfo.dependencies = config.dependencies || [];
|
|
182
|
+
moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
await prompts.log.warn(`Failed to read config for ${defaultName}: ${error.message}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return moduleInfo;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Find the source path for a module by searching all possible locations
|
|
192
|
+
* @param {string} moduleCode - Code of the module to find (from module.yaml)
|
|
193
|
+
* @returns {string|null} Path to the module source or null if not found
|
|
194
|
+
*/
|
|
195
|
+
async findModuleSource(moduleCode, options = {}) {
|
|
196
|
+
const projectRoot = getProjectRoot();
|
|
197
|
+
|
|
198
|
+
// Check for core module (directly under src/core-skills)
|
|
199
|
+
if (moduleCode === 'core') {
|
|
200
|
+
const corePath = getSourcePath('core-skills');
|
|
201
|
+
if (await fs.pathExists(corePath)) {
|
|
202
|
+
return corePath;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check for built-in bmm module (directly under src/bmm-skills)
|
|
207
|
+
if (moduleCode === 'bmm') {
|
|
208
|
+
const bmmPath = getSourcePath('bmm-skills');
|
|
209
|
+
if (await fs.pathExists(bmmPath)) {
|
|
210
|
+
return bmmPath;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check external official modules
|
|
215
|
+
const externalSource = await this.externalModuleManager.findExternalModuleSource(moduleCode, options);
|
|
216
|
+
if (externalSource) {
|
|
217
|
+
return externalSource;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Install a module
|
|
225
|
+
* @param {string} moduleName - Code of the module to install (from module.yaml)
|
|
226
|
+
* @param {string} bmadDir - Target bmad directory
|
|
227
|
+
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
|
228
|
+
* @param {Object} options - Additional installation options
|
|
229
|
+
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
|
|
230
|
+
* @param {Object} options.moduleConfig - Module configuration from config collector
|
|
231
|
+
* @param {Object} options.logger - Logger instance for output
|
|
232
|
+
*/
|
|
233
|
+
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
|
|
234
|
+
const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
|
|
235
|
+
const targetPath = path.join(bmadDir, moduleName);
|
|
236
|
+
|
|
237
|
+
if (!sourcePath) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Source for module '${moduleName}' is not available. It will be retained but cannot be updated without its source files.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (await fs.pathExists(targetPath)) {
|
|
244
|
+
await fs.remove(targetPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
|
|
248
|
+
|
|
249
|
+
if (!options.skipModuleInstaller) {
|
|
250
|
+
await this.createModuleDirectories(moduleName, bmadDir, options);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const { Manifest } = require('../core/manifest');
|
|
254
|
+
const manifestObj = new Manifest();
|
|
255
|
+
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
|
256
|
+
|
|
257
|
+
await manifestObj.addModule(bmadDir, moduleName, {
|
|
258
|
+
version: versionInfo.version,
|
|
259
|
+
source: versionInfo.source,
|
|
260
|
+
npmPackage: versionInfo.npmPackage,
|
|
261
|
+
repoUrl: versionInfo.repoUrl,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Update an existing module
|
|
269
|
+
* @param {string} moduleName - Name of the module to update
|
|
270
|
+
* @param {string} bmadDir - Target bmad directory
|
|
19
271
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
272
|
+
async update(moduleName, bmadDir) {
|
|
273
|
+
const sourcePath = await this.findModuleSource(moduleName);
|
|
274
|
+
const targetPath = path.join(bmadDir, moduleName);
|
|
275
|
+
|
|
276
|
+
if (!sourcePath) {
|
|
277
|
+
throw new Error(`Module '${moduleName}' not found in any source location`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!(await fs.pathExists(targetPath))) {
|
|
281
|
+
throw new Error(`Module '${moduleName}' is not installed`);
|
|
24
282
|
}
|
|
25
|
-
|
|
283
|
+
|
|
284
|
+
await this.syncModule(sourcePath, targetPath);
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
module: moduleName,
|
|
289
|
+
path: targetPath,
|
|
290
|
+
};
|
|
26
291
|
}
|
|
27
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Remove a module
|
|
295
|
+
* @param {string} moduleName - Name of the module to remove
|
|
296
|
+
* @param {string} bmadDir - Target bmad directory
|
|
297
|
+
*/
|
|
298
|
+
async remove(moduleName, bmadDir) {
|
|
299
|
+
const targetPath = path.join(bmadDir, moduleName);
|
|
300
|
+
|
|
301
|
+
if (!(await fs.pathExists(targetPath))) {
|
|
302
|
+
throw new Error(`Module '${moduleName}' is not installed`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await fs.remove(targetPath);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
module: moduleName,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Check if a module is installed
|
|
315
|
+
* @param {string} moduleName - Name of the module
|
|
316
|
+
* @param {string} bmadDir - Target bmad directory
|
|
317
|
+
* @returns {boolean} True if module is installed
|
|
318
|
+
*/
|
|
319
|
+
async isInstalled(moduleName, bmadDir) {
|
|
320
|
+
const targetPath = path.join(bmadDir, moduleName);
|
|
321
|
+
return await fs.pathExists(targetPath);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get installed module info
|
|
326
|
+
* @param {string} moduleName - Name of the module
|
|
327
|
+
* @param {string} bmadDir - Target bmad directory
|
|
328
|
+
* @returns {Object|null} Module info or null if not installed
|
|
329
|
+
*/
|
|
330
|
+
async getInstalledInfo(moduleName, bmadDir) {
|
|
331
|
+
const targetPath = path.join(bmadDir, moduleName);
|
|
332
|
+
|
|
333
|
+
if (!(await fs.pathExists(targetPath))) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const configPath = path.join(targetPath, 'config.yaml');
|
|
338
|
+
const moduleInfo = {
|
|
339
|
+
id: moduleName,
|
|
340
|
+
path: targetPath,
|
|
341
|
+
installed: true,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
if (await fs.pathExists(configPath)) {
|
|
345
|
+
try {
|
|
346
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
347
|
+
const config = yaml.parse(configContent);
|
|
348
|
+
Object.assign(moduleInfo, config);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
await prompts.log.warn(`Failed to read installed module config: ${error.message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return moduleInfo;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Copy module with filtering for localskip agents and conditional content
|
|
359
|
+
* @param {string} sourcePath - Source module path
|
|
360
|
+
* @param {string} targetPath - Target module path
|
|
361
|
+
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
|
362
|
+
* @param {Object} moduleConfig - Module configuration with conditional flags
|
|
363
|
+
*/
|
|
364
|
+
async copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback = null, moduleConfig = {}) {
|
|
365
|
+
// Get all files in source
|
|
366
|
+
const sourceFiles = await this.getFileList(sourcePath);
|
|
367
|
+
|
|
368
|
+
for (const file of sourceFiles) {
|
|
369
|
+
// Skip sub-modules directory - these are IDE-specific and handled separately
|
|
370
|
+
if (file.startsWith('sub-modules/')) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Skip sidecar directories - these contain agent-specific assets not needed at install time
|
|
375
|
+
const isInSidecarDirectory = path
|
|
376
|
+
.dirname(file)
|
|
377
|
+
.split('/')
|
|
378
|
+
.some((dir) => dir.toLowerCase().endsWith('-sidecar'));
|
|
379
|
+
|
|
380
|
+
if (isInSidecarDirectory) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Skip module.yaml at root - it's only needed at install time
|
|
385
|
+
if (file === 'module.yaml') {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Skip module root config.yaml only - generated by config collector with actual values
|
|
390
|
+
// Workflow-level config.yaml (e.g. workflows/orchestrate-story/config.yaml) must be copied
|
|
391
|
+
// for custom modules that use workflow-specific configuration
|
|
392
|
+
if (file === 'config.yaml') {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const sourceFile = path.join(sourcePath, file);
|
|
397
|
+
const targetFile = path.join(targetPath, file);
|
|
398
|
+
|
|
399
|
+
// Check if this is an agent file
|
|
400
|
+
if (file.startsWith('agents/') && file.endsWith('.md')) {
|
|
401
|
+
// Read the file to check for localskip
|
|
402
|
+
const content = await fs.readFile(sourceFile, 'utf8');
|
|
403
|
+
|
|
404
|
+
// Check for localskip="true" in the agent tag
|
|
405
|
+
const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
|
|
406
|
+
if (agentMatch) {
|
|
407
|
+
await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`);
|
|
408
|
+
continue; // Skip this agent
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Copy the file with placeholder replacement
|
|
413
|
+
await this.copyFile(sourceFile, targetFile);
|
|
414
|
+
|
|
415
|
+
// Track the file if callback provided
|
|
416
|
+
if (fileTrackingCallback) {
|
|
417
|
+
fileTrackingCallback(targetFile);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Find all .md agent files recursively in a directory
|
|
424
|
+
* @param {string} dir - Directory to search
|
|
425
|
+
* @returns {Array} List of .md agent file paths
|
|
426
|
+
*/
|
|
427
|
+
async findAgentMdFiles(dir) {
|
|
428
|
+
const agentFiles = [];
|
|
429
|
+
|
|
430
|
+
async function searchDirectory(searchDir) {
|
|
431
|
+
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
|
432
|
+
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
const fullPath = path.join(searchDir, entry.name);
|
|
435
|
+
|
|
436
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
437
|
+
agentFiles.push(fullPath);
|
|
438
|
+
} else if (entry.isDirectory()) {
|
|
439
|
+
await searchDirectory(fullPath);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await searchDirectory(dir);
|
|
445
|
+
return agentFiles;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Create directories declared in module.yaml's `directories` key
|
|
450
|
+
* This replaces the security-risky module installer pattern with declarative config
|
|
451
|
+
* During updates, if a directory path changed, moves the old directory to the new path
|
|
452
|
+
* @param {string} moduleName - Name of the module
|
|
453
|
+
* @param {string} bmadDir - Target bmad directory
|
|
454
|
+
* @param {Object} options - Installation options
|
|
455
|
+
* @param {Object} options.moduleConfig - Module configuration from config collector
|
|
456
|
+
* @param {Object} options.existingModuleConfig - Previous module config (for detecting path changes during updates)
|
|
457
|
+
* @param {Object} options.coreConfig - Core configuration
|
|
458
|
+
* @returns {Promise<{createdDirs: string[], movedDirs: string[], createdWdsFolders: string[]}>} Created directories info
|
|
459
|
+
*/
|
|
460
|
+
async createModuleDirectories(moduleName, bmadDir, options = {}) {
|
|
461
|
+
const moduleConfig = options.moduleConfig || {};
|
|
462
|
+
const existingModuleConfig = options.existingModuleConfig || {};
|
|
463
|
+
const projectRoot = path.dirname(bmadDir);
|
|
464
|
+
const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
|
465
|
+
|
|
466
|
+
// Special handling for core module - it's in src/core-skills not src/modules
|
|
467
|
+
let sourcePath;
|
|
468
|
+
if (moduleName === 'core') {
|
|
469
|
+
sourcePath = getSourcePath('core-skills');
|
|
470
|
+
} else {
|
|
471
|
+
sourcePath = await this.findModuleSource(moduleName, { silent: true });
|
|
472
|
+
if (!sourcePath) {
|
|
473
|
+
return emptyResult; // No source found, skip
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Read module.yaml to find the `directories` key
|
|
478
|
+
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
|
|
479
|
+
if (!(await fs.pathExists(moduleYamlPath))) {
|
|
480
|
+
return emptyResult; // No module.yaml, skip
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let moduleYaml;
|
|
484
|
+
try {
|
|
485
|
+
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
|
486
|
+
moduleYaml = yaml.parse(yamlContent);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
await prompts.log.warn(`Invalid module.yaml for ${moduleName}: ${error.message}`);
|
|
489
|
+
return emptyResult;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!moduleYaml || !moduleYaml.directories) {
|
|
493
|
+
return emptyResult; // No directories declared, skip
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const directories = moduleYaml.directories;
|
|
497
|
+
const wdsFolders = moduleYaml.wds_folders || [];
|
|
498
|
+
const createdDirs = [];
|
|
499
|
+
const movedDirs = [];
|
|
500
|
+
const createdWdsFolders = [];
|
|
501
|
+
|
|
502
|
+
for (const dirRef of directories) {
|
|
503
|
+
// Parse variable reference like "{design_artifacts}"
|
|
504
|
+
const varMatch = dirRef.match(/^\{([^}]+)\}$/);
|
|
505
|
+
if (!varMatch) {
|
|
506
|
+
// Not a variable reference, skip
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const configKey = varMatch[1];
|
|
511
|
+
const dirValue = moduleConfig[configKey];
|
|
512
|
+
if (!dirValue || typeof dirValue !== 'string') {
|
|
513
|
+
continue; // No value or not a string, skip
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Strip {project-root}/ prefix if present
|
|
517
|
+
let dirPath = dirValue.replace(/^\{project-root\}\/?/, '');
|
|
518
|
+
|
|
519
|
+
// Handle remaining {project-root} anywhere in the path
|
|
520
|
+
dirPath = dirPath.replaceAll('{project-root}', '');
|
|
521
|
+
|
|
522
|
+
// Resolve to absolute path
|
|
523
|
+
const fullPath = path.join(projectRoot, dirPath);
|
|
524
|
+
|
|
525
|
+
// Validate path is within project root (prevent directory traversal)
|
|
526
|
+
const normalizedPath = path.normalize(fullPath);
|
|
527
|
+
const normalizedRoot = path.normalize(projectRoot);
|
|
528
|
+
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
|
|
529
|
+
const color = await prompts.getColor();
|
|
530
|
+
await prompts.log.warn(color.yellow(`${configKey} path escapes project root, skipping: ${dirPath}`));
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Check if directory path changed from previous config (update/modify scenario)
|
|
535
|
+
const oldDirValue = existingModuleConfig[configKey];
|
|
536
|
+
let oldFullPath = null;
|
|
537
|
+
let oldDirPath = null;
|
|
538
|
+
if (oldDirValue && typeof oldDirValue === 'string') {
|
|
539
|
+
// F3: Normalize both values before comparing to avoid false negatives
|
|
540
|
+
// from trailing slashes, separator differences, or prefix format variations
|
|
541
|
+
let normalizedOld = oldDirValue.replace(/^\{project-root\}\/?/, '');
|
|
542
|
+
normalizedOld = path.normalize(normalizedOld.replaceAll('{project-root}', ''));
|
|
543
|
+
const normalizedNew = path.normalize(dirPath);
|
|
544
|
+
|
|
545
|
+
if (normalizedOld !== normalizedNew) {
|
|
546
|
+
oldDirPath = normalizedOld;
|
|
547
|
+
oldFullPath = path.join(projectRoot, oldDirPath);
|
|
548
|
+
const normalizedOldAbsolute = path.normalize(oldFullPath);
|
|
549
|
+
if (!normalizedOldAbsolute.startsWith(normalizedRoot + path.sep) && normalizedOldAbsolute !== normalizedRoot) {
|
|
550
|
+
oldFullPath = null; // Old path escapes project root, ignore it
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// F13: Prevent parent/child move (e.g. docs/planning → docs/planning/v2)
|
|
554
|
+
if (oldFullPath) {
|
|
555
|
+
const normalizedNewAbsolute = path.normalize(fullPath);
|
|
556
|
+
if (
|
|
557
|
+
normalizedOldAbsolute.startsWith(normalizedNewAbsolute + path.sep) ||
|
|
558
|
+
normalizedNewAbsolute.startsWith(normalizedOldAbsolute + path.sep)
|
|
559
|
+
) {
|
|
560
|
+
const color = await prompts.getColor();
|
|
561
|
+
await prompts.log.warn(
|
|
562
|
+
color.yellow(
|
|
563
|
+
`${configKey}: cannot move between parent/child paths (${oldDirPath} / ${dirPath}), creating new directory instead`,
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
oldFullPath = null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const dirName = configKey.replaceAll('_', ' ');
|
|
573
|
+
|
|
574
|
+
if (oldFullPath && (await fs.pathExists(oldFullPath)) && !(await fs.pathExists(fullPath))) {
|
|
575
|
+
// Path changed and old dir exists → move old to new location
|
|
576
|
+
// F1: Use fs.move() instead of fs.rename() for cross-device/volume support
|
|
577
|
+
// F2: Wrap in try/catch — fallback to creating new dir on failure
|
|
578
|
+
try {
|
|
579
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
580
|
+
await fs.move(oldFullPath, fullPath);
|
|
581
|
+
movedDirs.push(`${dirName}: ${oldDirPath} → ${dirPath}`);
|
|
582
|
+
} catch (moveError) {
|
|
583
|
+
const color = await prompts.getColor();
|
|
584
|
+
await prompts.log.warn(
|
|
585
|
+
color.yellow(
|
|
586
|
+
`Failed to move ${oldDirPath} → ${dirPath}: ${moveError.message}\n Creating new directory instead. Please move contents from the old directory manually.`,
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
await fs.ensureDir(fullPath);
|
|
590
|
+
createdDirs.push(`${dirName}: ${dirPath}`);
|
|
591
|
+
}
|
|
592
|
+
} else if (oldFullPath && (await fs.pathExists(oldFullPath)) && (await fs.pathExists(fullPath))) {
|
|
593
|
+
// F5: Both old and new directories exist — warn user about potential orphaned documents
|
|
594
|
+
const color = await prompts.getColor();
|
|
595
|
+
await prompts.log.warn(
|
|
596
|
+
color.yellow(
|
|
597
|
+
`${dirName}: path changed but both directories exist:\n Old: ${oldDirPath}\n New: ${dirPath}\n Old directory may contain orphaned documents — please review and merge manually.`,
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
} else if (!(await fs.pathExists(fullPath))) {
|
|
601
|
+
// New directory doesn't exist yet → create it
|
|
602
|
+
createdDirs.push(`${dirName}: ${dirPath}`);
|
|
603
|
+
await fs.ensureDir(fullPath);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Create WDS subfolders if this is the design_artifacts directory
|
|
607
|
+
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
|
|
608
|
+
for (const subfolder of wdsFolders) {
|
|
609
|
+
const subPath = path.join(fullPath, subfolder);
|
|
610
|
+
if (!(await fs.pathExists(subPath))) {
|
|
611
|
+
await fs.ensureDir(subPath);
|
|
612
|
+
createdWdsFolders.push(subfolder);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return { createdDirs, movedDirs, createdWdsFolders };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Private: Process module configuration
|
|
623
|
+
* @param {string} modulePath - Path to installed module
|
|
624
|
+
* @param {string} moduleName - Module name
|
|
625
|
+
*/
|
|
626
|
+
async processModuleConfig(modulePath, moduleName) {
|
|
627
|
+
const configPath = path.join(modulePath, 'config.yaml');
|
|
628
|
+
|
|
629
|
+
if (await fs.pathExists(configPath)) {
|
|
630
|
+
try {
|
|
631
|
+
let configContent = await fs.readFile(configPath, 'utf8');
|
|
632
|
+
|
|
633
|
+
// Replace path placeholders
|
|
634
|
+
configContent = configContent.replaceAll('{project-root}', `bmad/${moduleName}`);
|
|
635
|
+
configContent = configContent.replaceAll('{module}', moduleName);
|
|
636
|
+
|
|
637
|
+
await fs.writeFile(configPath, configContent, 'utf8');
|
|
638
|
+
} catch (error) {
|
|
639
|
+
await prompts.log.warn(`Failed to process module config: ${error.message}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Private: Sync module files (preserving user modifications)
|
|
646
|
+
* @param {string} sourcePath - Source module path
|
|
647
|
+
* @param {string} targetPath - Target module path
|
|
648
|
+
*/
|
|
649
|
+
async syncModule(sourcePath, targetPath) {
|
|
650
|
+
// Get list of all source files
|
|
651
|
+
const sourceFiles = await this.getFileList(sourcePath);
|
|
652
|
+
|
|
653
|
+
for (const file of sourceFiles) {
|
|
654
|
+
const sourceFile = path.join(sourcePath, file);
|
|
655
|
+
const targetFile = path.join(targetPath, file);
|
|
656
|
+
|
|
657
|
+
// Check if target file exists and has been modified
|
|
658
|
+
if (await fs.pathExists(targetFile)) {
|
|
659
|
+
const sourceStats = await fs.stat(sourceFile);
|
|
660
|
+
const targetStats = await fs.stat(targetFile);
|
|
661
|
+
|
|
662
|
+
// Skip if target is newer (user modified)
|
|
663
|
+
if (targetStats.mtime > sourceStats.mtime) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Copy file with placeholder replacement
|
|
669
|
+
await this.copyFile(sourceFile, targetFile);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Private: Get list of all files in a directory
|
|
675
|
+
* @param {string} dir - Directory path
|
|
676
|
+
* @param {string} baseDir - Base directory for relative paths
|
|
677
|
+
* @returns {Array} List of relative file paths
|
|
678
|
+
*/
|
|
679
|
+
async getFileList(dir, baseDir = dir) {
|
|
680
|
+
const files = [];
|
|
681
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
682
|
+
|
|
683
|
+
for (const entry of entries) {
|
|
684
|
+
const fullPath = path.join(dir, entry.name);
|
|
685
|
+
|
|
686
|
+
if (entry.isDirectory()) {
|
|
687
|
+
const subFiles = await this.getFileList(fullPath, baseDir);
|
|
688
|
+
files.push(...subFiles);
|
|
689
|
+
} else {
|
|
690
|
+
files.push(path.relative(baseDir, fullPath));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return files;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ─── Config collection methods (merged from ConfigCollector) ───
|
|
698
|
+
|
|
28
699
|
/**
|
|
29
700
|
* Find the bmad installation directory in a project
|
|
30
701
|
* V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
|
|
@@ -95,7 +766,7 @@ class ConfigCollector {
|
|
|
95
766
|
* @param {string} projectDir - Target project directory
|
|
96
767
|
*/
|
|
97
768
|
async loadExistingConfig(projectDir) {
|
|
98
|
-
this.
|
|
769
|
+
this._existingConfig = {};
|
|
99
770
|
|
|
100
771
|
// Check if project directory exists first
|
|
101
772
|
if (!(await fs.pathExists(projectDir))) {
|
|
@@ -129,7 +800,7 @@ class ConfigCollector {
|
|
|
129
800
|
const content = await fs.readFile(moduleConfigPath, 'utf8');
|
|
130
801
|
const moduleConfig = yaml.parse(content);
|
|
131
802
|
if (moduleConfig) {
|
|
132
|
-
this.
|
|
803
|
+
this._existingConfig[entry.name] = moduleConfig;
|
|
133
804
|
foundAny = true;
|
|
134
805
|
}
|
|
135
806
|
} catch {
|
|
@@ -153,7 +824,7 @@ class ConfigCollector {
|
|
|
153
824
|
const results = [];
|
|
154
825
|
|
|
155
826
|
for (const moduleName of modules) {
|
|
156
|
-
// Resolve module.yaml path - custom paths first, then standard location, then
|
|
827
|
+
// Resolve module.yaml path - custom paths first, then standard location, then OfficialModules search
|
|
157
828
|
let moduleConfigPath = null;
|
|
158
829
|
const customPath = this.customModulePaths?.get(moduleName);
|
|
159
830
|
if (customPath) {
|
|
@@ -163,7 +834,7 @@ class ConfigCollector {
|
|
|
163
834
|
if (await fs.pathExists(standardPath)) {
|
|
164
835
|
moduleConfigPath = standardPath;
|
|
165
836
|
} else {
|
|
166
|
-
const moduleSourcePath = await this.
|
|
837
|
+
const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
|
|
167
838
|
if (moduleSourcePath) {
|
|
168
839
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
|
169
840
|
}
|
|
@@ -349,7 +1020,7 @@ class ConfigCollector {
|
|
|
349
1020
|
this.currentProjectDir = projectDir;
|
|
350
1021
|
|
|
351
1022
|
// Load existing config if not already loaded
|
|
352
|
-
if (!this.
|
|
1023
|
+
if (!this._existingConfig) {
|
|
353
1024
|
await this.loadExistingConfig(projectDir);
|
|
354
1025
|
}
|
|
355
1026
|
|
|
@@ -364,7 +1035,7 @@ class ConfigCollector {
|
|
|
364
1035
|
|
|
365
1036
|
// If not found in src/modules, we need to find it by searching the project
|
|
366
1037
|
if (!(await fs.pathExists(moduleConfigPath))) {
|
|
367
|
-
const moduleSourcePath = await this.
|
|
1038
|
+
const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
|
|
368
1039
|
|
|
369
1040
|
if (moduleSourcePath) {
|
|
370
1041
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
|
@@ -378,7 +1049,7 @@ class ConfigCollector {
|
|
|
378
1049
|
configPath = moduleConfigPath;
|
|
379
1050
|
} else {
|
|
380
1051
|
// Check if this is a custom module with custom.yaml
|
|
381
|
-
const moduleSourcePath = await this.
|
|
1052
|
+
const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
|
|
382
1053
|
|
|
383
1054
|
if (moduleSourcePath) {
|
|
384
1055
|
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
|
|
@@ -391,11 +1062,11 @@ class ConfigCollector {
|
|
|
391
1062
|
}
|
|
392
1063
|
|
|
393
1064
|
// No config schema for this module - use existing values
|
|
394
|
-
if (this.
|
|
1065
|
+
if (this._existingConfig && this._existingConfig[moduleName]) {
|
|
395
1066
|
if (!this.collectedConfig[moduleName]) {
|
|
396
1067
|
this.collectedConfig[moduleName] = {};
|
|
397
1068
|
}
|
|
398
|
-
this.collectedConfig[moduleName] = { ...this.
|
|
1069
|
+
this.collectedConfig[moduleName] = { ...this._existingConfig[moduleName] };
|
|
399
1070
|
}
|
|
400
1071
|
return false;
|
|
401
1072
|
}
|
|
@@ -409,7 +1080,7 @@ class ConfigCollector {
|
|
|
409
1080
|
|
|
410
1081
|
// Compare schema with existing config to find new/missing fields
|
|
411
1082
|
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
|
|
412
|
-
const existingKeys = this.
|
|
1083
|
+
const existingKeys = this._existingConfig && this._existingConfig[moduleName] ? Object.keys(this._existingConfig[moduleName]) : [];
|
|
413
1084
|
|
|
414
1085
|
// Check if this module has no configuration keys at all (like CIS)
|
|
415
1086
|
// Filter out metadata fields and only count actual config objects
|
|
@@ -440,11 +1111,11 @@ class ConfigCollector {
|
|
|
440
1111
|
|
|
441
1112
|
// If in silent mode and no new keys (neither interactive nor static), use existing config and skip prompts
|
|
442
1113
|
if (silentMode && newKeys.length === 0 && newStaticKeys.length === 0) {
|
|
443
|
-
if (this.
|
|
1114
|
+
if (this._existingConfig && this._existingConfig[moduleName]) {
|
|
444
1115
|
if (!this.collectedConfig[moduleName]) {
|
|
445
1116
|
this.collectedConfig[moduleName] = {};
|
|
446
1117
|
}
|
|
447
|
-
this.collectedConfig[moduleName] = { ...this.
|
|
1118
|
+
this.collectedConfig[moduleName] = { ...this._existingConfig[moduleName] };
|
|
448
1119
|
|
|
449
1120
|
// Special handling for user_name: ensure it has a value
|
|
450
1121
|
if (
|
|
@@ -455,7 +1126,7 @@ class ConfigCollector {
|
|
|
455
1126
|
}
|
|
456
1127
|
|
|
457
1128
|
// Also populate allAnswers for cross-referencing
|
|
458
|
-
for (const [key, value] of Object.entries(this.
|
|
1129
|
+
for (const [key, value] of Object.entries(this._existingConfig[moduleName])) {
|
|
459
1130
|
// Ensure user_name is properly set in allAnswers too
|
|
460
1131
|
let finalValue = value;
|
|
461
1132
|
if (moduleName === 'core' && key === 'user_name' && (!value || value === '[USER_NAME]')) {
|
|
@@ -519,8 +1190,8 @@ class ConfigCollector {
|
|
|
519
1190
|
|
|
520
1191
|
// Process all answers (both static and prompted)
|
|
521
1192
|
// First, copy existing config to preserve values that aren't being updated
|
|
522
|
-
if (this.
|
|
523
|
-
this.collectedConfig[moduleName] = { ...this.
|
|
1193
|
+
if (this._existingConfig && this._existingConfig[moduleName]) {
|
|
1194
|
+
this.collectedConfig[moduleName] = { ...this._existingConfig[moduleName] };
|
|
524
1195
|
} else {
|
|
525
1196
|
this.collectedConfig[moduleName] = {};
|
|
526
1197
|
}
|
|
@@ -545,11 +1216,11 @@ class ConfigCollector {
|
|
|
545
1216
|
}
|
|
546
1217
|
|
|
547
1218
|
// Copy over existing values for fields that weren't prompted
|
|
548
|
-
if (this.
|
|
1219
|
+
if (this._existingConfig && this._existingConfig[moduleName]) {
|
|
549
1220
|
if (!this.collectedConfig[moduleName]) {
|
|
550
1221
|
this.collectedConfig[moduleName] = {};
|
|
551
1222
|
}
|
|
552
|
-
for (const [key, value] of Object.entries(this.
|
|
1223
|
+
for (const [key, value] of Object.entries(this._existingConfig[moduleName])) {
|
|
553
1224
|
if (!this.collectedConfig[moduleName][key]) {
|
|
554
1225
|
this.collectedConfig[moduleName][key] = value;
|
|
555
1226
|
this.allAnswers[`${moduleName}_${key}`] = value;
|
|
@@ -652,7 +1323,7 @@ class ConfigCollector {
|
|
|
652
1323
|
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
|
653
1324
|
this.currentProjectDir = projectDir;
|
|
654
1325
|
// Load existing config if needed and not already loaded
|
|
655
|
-
if (!skipLoadExisting && !this.
|
|
1326
|
+
if (!skipLoadExisting && !this._existingConfig) {
|
|
656
1327
|
await this.loadExistingConfig(projectDir);
|
|
657
1328
|
}
|
|
658
1329
|
|
|
@@ -674,7 +1345,7 @@ class ConfigCollector {
|
|
|
674
1345
|
|
|
675
1346
|
// If not found in src/modules or custom paths, search the project
|
|
676
1347
|
if (!(await fs.pathExists(moduleConfigPath))) {
|
|
677
|
-
const moduleSourcePath = await this.
|
|
1348
|
+
const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
|
|
678
1349
|
|
|
679
1350
|
if (moduleSourcePath) {
|
|
680
1351
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
|
@@ -994,8 +1665,8 @@ class ConfigCollector {
|
|
|
994
1665
|
}
|
|
995
1666
|
|
|
996
1667
|
// Prefer the current module's persisted value when re-prompting an existing install
|
|
997
|
-
if (!configValue && currentModule && this.
|
|
998
|
-
configValue = this.
|
|
1668
|
+
if (!configValue && currentModule && this._existingConfig?.[currentModule]?.[configKey] !== undefined) {
|
|
1669
|
+
configValue = this._existingConfig[currentModule][configKey];
|
|
999
1670
|
}
|
|
1000
1671
|
|
|
1001
1672
|
// Check in already collected config
|
|
@@ -1009,10 +1680,10 @@ class ConfigCollector {
|
|
|
1009
1680
|
}
|
|
1010
1681
|
|
|
1011
1682
|
// Fall back to other existing module config values
|
|
1012
|
-
if (!configValue && this.
|
|
1013
|
-
for (const mod of Object.keys(this.
|
|
1014
|
-
if (mod !== '_meta' && this.
|
|
1015
|
-
configValue = this.
|
|
1683
|
+
if (!configValue && this._existingConfig) {
|
|
1684
|
+
for (const mod of Object.keys(this._existingConfig)) {
|
|
1685
|
+
if (mod !== '_meta' && this._existingConfig[mod] && this._existingConfig[mod][configKey]) {
|
|
1686
|
+
configValue = this._existingConfig[mod][configKey];
|
|
1016
1687
|
break;
|
|
1017
1688
|
}
|
|
1018
1689
|
}
|
|
@@ -1083,8 +1754,8 @@ class ConfigCollector {
|
|
|
1083
1754
|
|
|
1084
1755
|
// Check for existing value
|
|
1085
1756
|
let existingValue = null;
|
|
1086
|
-
if (this.
|
|
1087
|
-
existingValue = this.
|
|
1757
|
+
if (this._existingConfig && this._existingConfig[moduleName]) {
|
|
1758
|
+
existingValue = this._existingConfig[moduleName][key];
|
|
1088
1759
|
existingValue = this.normalizeExistingValueForPrompt(existingValue, moduleName, item, moduleConfig);
|
|
1089
1760
|
}
|
|
1090
1761
|
|
|
@@ -1369,4 +2040,4 @@ class ConfigCollector {
|
|
|
1369
2040
|
}
|
|
1370
2041
|
}
|
|
1371
2042
|
|
|
1372
|
-
module.exports = {
|
|
2043
|
+
module.exports = { OfficialModules };
|