bmad-method 6.3.1-next.19 → 6.3.1-next.20
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
CHANGED
|
@@ -11,6 +11,7 @@ const prompts = require('../prompts');
|
|
|
11
11
|
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|
12
12
|
const { InstallPaths } = require('./install-paths');
|
|
13
13
|
const { ExternalModuleManager } = require('../modules/external-manager');
|
|
14
|
+
const { resolveModuleVersion } = require('../modules/version-resolver');
|
|
14
15
|
|
|
15
16
|
const { ExistingInstall } = require('./existing-install');
|
|
16
17
|
|
|
@@ -24,44 +25,6 @@ class Installer {
|
|
|
24
25
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
* Read the module version from .claude-plugin/marketplace.json
|
|
29
|
-
* Walks up from sourcePath looking for .claude-plugin/marketplace.json
|
|
30
|
-
* @param {string} sourcePath - Module source directory
|
|
31
|
-
* @returns {string} Version string or empty string
|
|
32
|
-
*/
|
|
33
|
-
async _getMarketplaceVersion(sourcePath) {
|
|
34
|
-
let dir = sourcePath;
|
|
35
|
-
for (let i = 0; i < 5; i++) {
|
|
36
|
-
const marketplacePath = path.join(dir, '.claude-plugin', 'marketplace.json');
|
|
37
|
-
if (await fs.pathExists(marketplacePath)) {
|
|
38
|
-
try {
|
|
39
|
-
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
|
40
|
-
return this._extractMarketplaceVersion(data);
|
|
41
|
-
} catch {
|
|
42
|
-
return '';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const parent = path.dirname(dir);
|
|
46
|
-
if (parent === dir) break;
|
|
47
|
-
dir = parent;
|
|
48
|
-
}
|
|
49
|
-
return '';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Extract the highest version from marketplace.json plugins array
|
|
54
|
-
*/
|
|
55
|
-
_extractMarketplaceVersion(data) {
|
|
56
|
-
const plugins = data?.plugins;
|
|
57
|
-
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
|
58
|
-
let best = '';
|
|
59
|
-
for (const p of plugins) {
|
|
60
|
-
if (p.version && (!best || p.version > best)) best = p.version;
|
|
61
|
-
}
|
|
62
|
-
return best;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
28
|
/**
|
|
66
29
|
* Main installation method
|
|
67
30
|
* @param {Object} config - Installation configuration
|
|
@@ -641,15 +604,18 @@ class Installer {
|
|
|
641
604
|
},
|
|
642
605
|
);
|
|
643
606
|
|
|
644
|
-
// Get display name from source module.yaml
|
|
607
|
+
// Get display name from source module.yaml and resolve the freshest version metadata we can find locally.
|
|
645
608
|
const sourcePath = await officialModules.findModuleSource(moduleName, { silent: true });
|
|
646
609
|
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
|
|
647
610
|
const displayName = moduleInfo?.name || moduleName;
|
|
648
611
|
|
|
649
|
-
// Prefer version from resolution cache (accurate for custom/local modules),
|
|
650
|
-
// fall back to marketplace.json walk-up for official modules
|
|
651
612
|
const cachedResolution = CustomModuleManager._resolutionCache.get(moduleName);
|
|
652
|
-
const
|
|
613
|
+
const versionInfo = await resolveModuleVersion(moduleName, {
|
|
614
|
+
moduleSourcePath: sourcePath,
|
|
615
|
+
fallbackVersion: cachedResolution?.version,
|
|
616
|
+
marketplacePluginNames: cachedResolution?.pluginName ? [cachedResolution.pluginName] : [],
|
|
617
|
+
});
|
|
618
|
+
const version = versionInfo.version || '';
|
|
653
619
|
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
|
654
620
|
}
|
|
655
621
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
const fs = require('../fs-native');
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
|
-
const {
|
|
4
|
+
const { resolveModuleVersion } = require('../modules/version-resolver');
|
|
5
5
|
const prompts = require('../prompts');
|
|
6
6
|
|
|
7
7
|
class Manifest {
|
|
@@ -258,13 +258,11 @@ class Manifest {
|
|
|
258
258
|
* @returns {Object} Version info object with version, source, npmPackage, repoUrl
|
|
259
259
|
*/
|
|
260
260
|
async getModuleVersionInfo(moduleName, bmadDir, moduleSourcePath = null) {
|
|
261
|
-
const yaml = require('yaml');
|
|
262
|
-
|
|
263
261
|
// Resolve source type first, then read version with the correct path context
|
|
264
262
|
if (['core', 'bmm'].includes(moduleName)) {
|
|
265
|
-
const
|
|
263
|
+
const versionInfo = await resolveModuleVersion(moduleName, { moduleSourcePath });
|
|
266
264
|
return {
|
|
267
|
-
version,
|
|
265
|
+
version: versionInfo.version,
|
|
268
266
|
source: 'built-in',
|
|
269
267
|
npmPackage: null,
|
|
270
268
|
repoUrl: null,
|
|
@@ -277,10 +275,9 @@ class Manifest {
|
|
|
277
275
|
const moduleInfo = await extMgr.getModuleByCode(moduleName);
|
|
278
276
|
|
|
279
277
|
if (moduleInfo) {
|
|
280
|
-
|
|
281
|
-
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
|
278
|
+
const versionInfo = await resolveModuleVersion(moduleName, { moduleSourcePath });
|
|
282
279
|
return {
|
|
283
|
-
version,
|
|
280
|
+
version: versionInfo.version,
|
|
284
281
|
source: 'external',
|
|
285
282
|
npmPackage: moduleInfo.npmPackage || null,
|
|
286
283
|
repoUrl: moduleInfo.url || null,
|
|
@@ -292,9 +289,12 @@ class Manifest {
|
|
|
292
289
|
const communityMgr = new CommunityModuleManager();
|
|
293
290
|
const communityInfo = await communityMgr.getModuleByCode(moduleName);
|
|
294
291
|
if (communityInfo) {
|
|
295
|
-
const
|
|
292
|
+
const versionInfo = await resolveModuleVersion(moduleName, {
|
|
293
|
+
moduleSourcePath,
|
|
294
|
+
fallbackVersion: communityInfo.version,
|
|
295
|
+
});
|
|
296
296
|
return {
|
|
297
|
-
version:
|
|
297
|
+
version: versionInfo.version || communityInfo.version,
|
|
298
298
|
source: 'community',
|
|
299
299
|
npmPackage: communityInfo.npmPackage || null,
|
|
300
300
|
repoUrl: communityInfo.url || null,
|
|
@@ -307,9 +307,13 @@ class Manifest {
|
|
|
307
307
|
const resolved = customMgr.getResolution(moduleName);
|
|
308
308
|
const customSource = await customMgr.findModuleSourceByCode(moduleName, { bmadDir });
|
|
309
309
|
if (customSource || resolved) {
|
|
310
|
-
const
|
|
310
|
+
const versionInfo = await resolveModuleVersion(moduleName, {
|
|
311
|
+
moduleSourcePath: moduleSourcePath || customSource,
|
|
312
|
+
fallbackVersion: resolved?.version,
|
|
313
|
+
marketplacePluginNames: resolved?.pluginName ? [resolved.pluginName] : [],
|
|
314
|
+
});
|
|
311
315
|
return {
|
|
312
|
-
version:
|
|
316
|
+
version: versionInfo.version,
|
|
313
317
|
source: 'custom',
|
|
314
318
|
npmPackage: null,
|
|
315
319
|
repoUrl: resolved?.repoUrl || null,
|
|
@@ -318,64 +322,15 @@ class Manifest {
|
|
|
318
322
|
}
|
|
319
323
|
|
|
320
324
|
// Unknown module
|
|
321
|
-
const
|
|
325
|
+
const versionInfo = await resolveModuleVersion(moduleName, { moduleSourcePath });
|
|
322
326
|
return {
|
|
323
|
-
version,
|
|
327
|
+
version: versionInfo.version,
|
|
324
328
|
source: 'unknown',
|
|
325
329
|
npmPackage: null,
|
|
326
330
|
repoUrl: null,
|
|
327
331
|
};
|
|
328
332
|
}
|
|
329
333
|
|
|
330
|
-
/**
|
|
331
|
-
* Read version from .claude-plugin/marketplace.json for a module
|
|
332
|
-
* @param {string} moduleName - Module code
|
|
333
|
-
* @returns {string|null} Version or null
|
|
334
|
-
*/
|
|
335
|
-
async _readMarketplaceVersion(moduleName, moduleSourcePath = null) {
|
|
336
|
-
const os = require('node:os');
|
|
337
|
-
let marketplacePath;
|
|
338
|
-
|
|
339
|
-
if (['core', 'bmm'].includes(moduleName)) {
|
|
340
|
-
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
|
341
|
-
} else if (moduleSourcePath) {
|
|
342
|
-
// Walk up from source path to find marketplace.json
|
|
343
|
-
let dir = moduleSourcePath;
|
|
344
|
-
for (let i = 0; i < 5; i++) {
|
|
345
|
-
const candidate = path.join(dir, '.claude-plugin', 'marketplace.json');
|
|
346
|
-
if (await fs.pathExists(candidate)) {
|
|
347
|
-
marketplacePath = candidate;
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
const parent = path.dirname(dir);
|
|
351
|
-
if (parent === dir) break;
|
|
352
|
-
dir = parent;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Fallback to external module cache
|
|
357
|
-
if (!marketplacePath) {
|
|
358
|
-
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleName);
|
|
359
|
-
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
if (await fs.pathExists(marketplacePath)) {
|
|
364
|
-
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
|
365
|
-
const plugins = data?.plugins;
|
|
366
|
-
if (!Array.isArray(plugins) || plugins.length === 0) return null;
|
|
367
|
-
let best = null;
|
|
368
|
-
for (const p of plugins) {
|
|
369
|
-
if (p.version && (!best || p.version > best)) best = p.version;
|
|
370
|
-
}
|
|
371
|
-
return best;
|
|
372
|
-
}
|
|
373
|
-
} catch {
|
|
374
|
-
// ignore
|
|
375
|
-
}
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
334
|
/**
|
|
380
335
|
* Fetch latest version from npm for a package
|
|
381
336
|
* @param {string} packageName - npm package name
|
|
@@ -424,6 +379,7 @@ class Manifest {
|
|
|
424
379
|
* @returns {Array} Array of update info objects
|
|
425
380
|
*/
|
|
426
381
|
async checkForUpdates(bmadDir) {
|
|
382
|
+
const semver = require('semver');
|
|
427
383
|
const modules = await this.getAllModuleVersions(bmadDir);
|
|
428
384
|
const updates = [];
|
|
429
385
|
|
|
@@ -437,7 +393,10 @@ class Manifest {
|
|
|
437
393
|
continue;
|
|
438
394
|
}
|
|
439
395
|
|
|
440
|
-
|
|
396
|
+
const installedVersion = semver.valid(module.version) || semver.valid(semver.coerce(module.version || ''));
|
|
397
|
+
const availableVersion = semver.valid(latestVersion) || semver.valid(semver.coerce(latestVersion));
|
|
398
|
+
|
|
399
|
+
if (installedVersion && availableVersion && semver.gt(availableVersion, installedVersion)) {
|
|
441
400
|
updates.push({
|
|
442
401
|
name: module.name,
|
|
443
402
|
installedVersion: module.version,
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const semver = require('semver');
|
|
3
|
+
const yaml = require('yaml');
|
|
4
|
+
const fs = require('../fs-native');
|
|
5
|
+
const { getExternalModuleCachePath, getModulePath, resolveInstalledModuleYaml } = require('../project-root');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PARENT_DEPTH = 8;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a module version from authoritative on-disk metadata.
|
|
11
|
+
* Preference order:
|
|
12
|
+
* 1. package.json nearest the module source/cache root
|
|
13
|
+
* 2. module.yaml in the module source directory
|
|
14
|
+
* 3. .claude-plugin/marketplace.json
|
|
15
|
+
* 4. caller-provided fallback version
|
|
16
|
+
*
|
|
17
|
+
* @param {string} moduleName - Module code/name
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {string} [options.moduleSourcePath] - Directory containing module.yaml
|
|
20
|
+
* @param {string} [options.fallbackVersion] - Final fallback when no metadata is found
|
|
21
|
+
* @param {string[]} [options.marketplacePluginNames] - Preferred marketplace plugin names
|
|
22
|
+
* @returns {Promise<{version: string|null, source: string|null, path: string|null}>}
|
|
23
|
+
*/
|
|
24
|
+
async function resolveModuleVersion(moduleName, options = {}) {
|
|
25
|
+
const moduleSourcePath = await normalizeDirectoryPath(options.moduleSourcePath);
|
|
26
|
+
const packageJsonPath = await findPackageJsonPath(moduleName, moduleSourcePath);
|
|
27
|
+
|
|
28
|
+
if (packageJsonPath) {
|
|
29
|
+
const packageVersion = await readPackageJsonVersion(packageJsonPath);
|
|
30
|
+
if (packageVersion) {
|
|
31
|
+
return {
|
|
32
|
+
version: packageVersion,
|
|
33
|
+
source: 'package.json',
|
|
34
|
+
path: packageJsonPath,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const moduleYamlPath = await findModuleYamlPath(moduleName, moduleSourcePath);
|
|
40
|
+
if (moduleYamlPath) {
|
|
41
|
+
const moduleVersion = await readModuleYamlVersion(moduleYamlPath);
|
|
42
|
+
if (moduleVersion) {
|
|
43
|
+
return {
|
|
44
|
+
version: moduleVersion,
|
|
45
|
+
source: 'module.yaml',
|
|
46
|
+
path: moduleYamlPath,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const marketplaceVersion = await findMarketplaceVersion(moduleName, moduleSourcePath, options.marketplacePluginNames || []);
|
|
52
|
+
if (marketplaceVersion) {
|
|
53
|
+
return marketplaceVersion;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fallbackVersion = normalizeVersion(options.fallbackVersion);
|
|
57
|
+
if (fallbackVersion) {
|
|
58
|
+
return {
|
|
59
|
+
version: fallbackVersion,
|
|
60
|
+
source: 'fallback',
|
|
61
|
+
path: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
version: null,
|
|
67
|
+
source: null,
|
|
68
|
+
path: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function findPackageJsonPath(moduleName, moduleSourcePath) {
|
|
73
|
+
const roots = await buildSearchRoots(moduleName, moduleSourcePath);
|
|
74
|
+
|
|
75
|
+
for (const root of roots) {
|
|
76
|
+
const packageJsonPath = await findNearestUpwardFile(root.searchDir, 'package.json', { boundaryDir: root.boundaryDir });
|
|
77
|
+
if (packageJsonPath) {
|
|
78
|
+
return packageJsonPath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function findModuleYamlPath(moduleName, moduleSourcePath) {
|
|
86
|
+
if (moduleSourcePath) {
|
|
87
|
+
const directModuleYamlPath = path.join(moduleSourcePath, 'module.yaml');
|
|
88
|
+
if (await fs.pathExists(directModuleYamlPath)) {
|
|
89
|
+
return directModuleYamlPath;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return resolveInstalledModuleYaml(moduleName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function findMarketplaceVersion(moduleName, moduleSourcePath, marketplacePluginNames) {
|
|
97
|
+
const roots = await buildSearchRoots(moduleName, moduleSourcePath);
|
|
98
|
+
|
|
99
|
+
for (const root of roots) {
|
|
100
|
+
const marketplacePath = await findNearestUpwardFile(root.searchDir, path.join('.claude-plugin', 'marketplace.json'), {
|
|
101
|
+
boundaryDir: root.boundaryDir,
|
|
102
|
+
});
|
|
103
|
+
if (!marketplacePath) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const data = await readJsonFile(marketplacePath);
|
|
108
|
+
if (!data) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const version = extractMarketplaceVersion(data, moduleName, marketplacePluginNames);
|
|
113
|
+
if (version) {
|
|
114
|
+
return {
|
|
115
|
+
version,
|
|
116
|
+
source: 'marketplace.json',
|
|
117
|
+
path: marketplacePath,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function buildSearchRoots(moduleName, moduleSourcePath) {
|
|
126
|
+
const roots = [];
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
|
|
129
|
+
const addRoot = async (candidate) => {
|
|
130
|
+
const normalized = await normalizeExistingDirectory(candidate);
|
|
131
|
+
if (!normalized || seen.has(normalized)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
seen.add(normalized);
|
|
136
|
+
roots.push({
|
|
137
|
+
searchDir: normalized,
|
|
138
|
+
boundaryDir: await findSearchBoundary(normalized),
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
await addRoot(moduleSourcePath);
|
|
143
|
+
|
|
144
|
+
if (moduleName === 'core' || moduleName === 'bmm') {
|
|
145
|
+
await addRoot(getModulePath(moduleName));
|
|
146
|
+
} else {
|
|
147
|
+
await addRoot(getExternalModuleCachePath(moduleName));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return roots;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function findNearestUpwardFile(startDir, relativeFilePath, options = {}) {
|
|
154
|
+
const normalizedStartDir = await normalizeExistingDirectory(startDir);
|
|
155
|
+
if (!normalizedStartDir) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const maxDepth = options.maxDepth ?? DEFAULT_PARENT_DEPTH;
|
|
160
|
+
const normalizedBoundaryDir = await normalizeDirectoryPath(options.boundaryDir);
|
|
161
|
+
let currentDir = normalizedStartDir;
|
|
162
|
+
for (let depth = 0; depth <= maxDepth; depth++) {
|
|
163
|
+
const candidate = path.join(currentDir, relativeFilePath);
|
|
164
|
+
if (await fs.pathExists(candidate)) {
|
|
165
|
+
return candidate;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (normalizedBoundaryDir && currentDir === normalizedBoundaryDir) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const parentDir = path.dirname(currentDir);
|
|
173
|
+
if (parentDir === currentDir) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
currentDir = parentDir;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function findSearchBoundary(startDir) {
|
|
183
|
+
const normalizedStartDir = await normalizeExistingDirectory(startDir);
|
|
184
|
+
if (!normalizedStartDir) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let currentDir = normalizedStartDir;
|
|
189
|
+
for (let depth = 0; depth <= DEFAULT_PARENT_DEPTH; depth++) {
|
|
190
|
+
if (
|
|
191
|
+
(await fs.pathExists(path.join(currentDir, 'package.json'))) ||
|
|
192
|
+
(await fs.pathExists(path.join(currentDir, '.claude-plugin', 'marketplace.json'))) ||
|
|
193
|
+
(await fs.pathExists(path.join(currentDir, '.git')))
|
|
194
|
+
) {
|
|
195
|
+
return currentDir;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const parentDir = path.dirname(currentDir);
|
|
199
|
+
if (parentDir === currentDir) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
currentDir = parentDir;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return normalizedStartDir;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function normalizeDirectoryPath(candidate) {
|
|
209
|
+
if (!candidate) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const resolvedPath = path.resolve(candidate);
|
|
214
|
+
try {
|
|
215
|
+
const stats = await fs.stat(resolvedPath);
|
|
216
|
+
return stats.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
|
|
217
|
+
} catch {
|
|
218
|
+
return resolvedPath;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function normalizeExistingDirectory(candidate) {
|
|
223
|
+
const normalized = await normalizeDirectoryPath(candidate);
|
|
224
|
+
if (!normalized) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!(await fs.pathExists(normalized))) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return normalized;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function readPackageJsonVersion(packageJsonPath) {
|
|
236
|
+
const data = await readJsonFile(packageJsonPath);
|
|
237
|
+
return normalizeVersion(data?.version);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function readModuleYamlVersion(moduleYamlPath) {
|
|
241
|
+
try {
|
|
242
|
+
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
|
243
|
+
const data = yaml.parse(content);
|
|
244
|
+
return normalizeVersion(data?.version || data?.module_version || data?.moduleVersion);
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function readJsonFile(filePath) {
|
|
251
|
+
try {
|
|
252
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
253
|
+
return JSON.parse(content);
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function extractMarketplaceVersion(data, moduleName, marketplacePluginNames = []) {
|
|
260
|
+
const plugins = Array.isArray(data?.plugins) ? data.plugins : [];
|
|
261
|
+
if (plugins.length === 0) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const preferredNames = new Set(
|
|
266
|
+
[moduleName, ...marketplacePluginNames]
|
|
267
|
+
.filter((value) => typeof value === 'string')
|
|
268
|
+
.map((value) => value.trim())
|
|
269
|
+
.filter(Boolean),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const exactMatches = [];
|
|
273
|
+
const fallbackVersions = [];
|
|
274
|
+
|
|
275
|
+
for (const plugin of plugins) {
|
|
276
|
+
const version = normalizeVersion(plugin?.version);
|
|
277
|
+
if (!version) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fallbackVersions.push(version);
|
|
282
|
+
|
|
283
|
+
const pluginNames = [plugin?.name, plugin?.code].filter((value) => typeof value === 'string').map((value) => value.trim());
|
|
284
|
+
if (pluginNames.some((name) => preferredNames.has(name))) {
|
|
285
|
+
exactMatches.push(version);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return pickBestVersion(exactMatches.length > 0 ? exactMatches : fallbackVersions);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function pickBestVersion(versions) {
|
|
293
|
+
const candidates = versions.map(normalizeVersion).filter(Boolean);
|
|
294
|
+
if (candidates.length === 0) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
candidates.sort(compareVersionsDescending);
|
|
299
|
+
return candidates[0];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function compareVersionsDescending(left, right) {
|
|
303
|
+
const leftSemver = normalizeSemver(left);
|
|
304
|
+
const rightSemver = normalizeSemver(right);
|
|
305
|
+
|
|
306
|
+
if (leftSemver && rightSemver) {
|
|
307
|
+
return semver.rcompare(leftSemver, rightSemver);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (leftSemver) {
|
|
311
|
+
return -1;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (rightSemver) {
|
|
315
|
+
return 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return right.localeCompare(left, undefined, { numeric: true, sensitivity: 'base' });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function normalizeSemver(version) {
|
|
322
|
+
return semver.valid(version) || semver.valid(semver.coerce(version));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeVersion(version) {
|
|
326
|
+
if (typeof version !== 'string') {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const trimmed = version.trim();
|
|
331
|
+
return trimmed || null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
resolveModuleVersion,
|
|
336
|
+
};
|
package/tools/installer/ui.js
CHANGED
|
@@ -3,48 +3,17 @@ const os = require('node:os');
|
|
|
3
3
|
const fs = require('./fs-native');
|
|
4
4
|
const { CLIUtils } = require('./cli-utils');
|
|
5
5
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
|
6
|
-
const {
|
|
6
|
+
const { resolveModuleVersion } = require('./modules/version-resolver');
|
|
7
7
|
const prompts = require('./prompts');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Read module version from .
|
|
10
|
+
* Read a module version from the freshest local metadata available.
|
|
11
11
|
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
|
12
12
|
* @returns {string} Version string or empty string
|
|
13
13
|
*/
|
|
14
|
-
async function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
marketplacePath = path.join(getProjectRoot(), '.claude-plugin', 'marketplace.json');
|
|
18
|
-
} else {
|
|
19
|
-
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules', moduleCode);
|
|
20
|
-
marketplacePath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
if (await fs.pathExists(marketplacePath)) {
|
|
24
|
-
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
|
25
|
-
return _extractMarketplaceVersion(data);
|
|
26
|
-
}
|
|
27
|
-
} catch {
|
|
28
|
-
// ignore
|
|
29
|
-
}
|
|
30
|
-
return '';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Extract the highest version from marketplace.json plugins array.
|
|
35
|
-
* Handles multiple plugins per file safely.
|
|
36
|
-
* @param {Object} data - Parsed marketplace.json
|
|
37
|
-
* @returns {string} Version string or empty string
|
|
38
|
-
*/
|
|
39
|
-
function _extractMarketplaceVersion(data) {
|
|
40
|
-
const plugins = data?.plugins;
|
|
41
|
-
if (!Array.isArray(plugins) || plugins.length === 0) return '';
|
|
42
|
-
// Use the highest version across all plugins in the file
|
|
43
|
-
let best = '';
|
|
44
|
-
for (const p of plugins) {
|
|
45
|
-
if (p.version && (!best || p.version > best)) best = p.version;
|
|
46
|
-
}
|
|
47
|
-
return best;
|
|
14
|
+
async function getModuleVersion(moduleCode) {
|
|
15
|
+
const versionInfo = await resolveModuleVersion(moduleCode);
|
|
16
|
+
return versionInfo.version || '';
|
|
48
17
|
}
|
|
49
18
|
|
|
50
19
|
/**
|
|
@@ -644,7 +613,7 @@ class UI {
|
|
|
644
613
|
|
|
645
614
|
const buildModuleEntry = async (code, name, description, isDefault) => {
|
|
646
615
|
const isInstalled = installedModuleIds.has(code);
|
|
647
|
-
const version = await
|
|
616
|
+
const version = await getModuleVersion(code);
|
|
648
617
|
const label = version ? `${name} (v${version})` : name;
|
|
649
618
|
return {
|
|
650
619
|
label,
|