bmad-method 6.3.1-next.8 → 6.4.0
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 +3 -2
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +51 -36
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +90 -0
- package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +50 -33
- package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/customize.toml +81 -0
- package/src/bmm-skills/1-analysis/bmad-document-project/SKILL.md +57 -1
- package/src/bmm-skills/1-analysis/bmad-document-project/customize.toml +41 -0
- package/src/bmm-skills/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md +1 -0
- package/src/bmm-skills/1-analysis/bmad-document-project/workflows/full-scan-instructions.md +1 -0
- package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +48 -9
- package/src/bmm-skills/1-analysis/bmad-prfaq/customize.toml +41 -0
- package/src/bmm-skills/1-analysis/bmad-prfaq/references/verdict.md +4 -0
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +44 -9
- package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +47 -0
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/contextual-discovery.md +8 -7
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/draft-and-review.md +6 -5
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/finalize.md +4 -1
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/guided-elicitation.md +3 -2
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/SKILL.md +91 -1
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +41 -0
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +6 -0
- package/src/bmm-skills/1-analysis/research/bmad-market-research/SKILL.md +91 -1
- package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +41 -0
- package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +6 -0
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/SKILL.md +91 -1
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +41 -0
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +6 -0
- package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +50 -35
- package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/customize.toml +85 -0
- package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +50 -31
- package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/customize.toml +60 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-prd/SKILL.md +99 -1
- package/src/bmm-skills/2-plan-workflows/bmad-create-prd/customize.toml +41 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md +6 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/SKILL.md +70 -1
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +41 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/SKILL.md +97 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +42 -0
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
- package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/SKILL.md +99 -1
- package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +42 -0
- package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +50 -30
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +65 -0
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/SKILL.md +86 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +41 -0
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +69 -1
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +41 -0
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/SKILL.md +88 -1
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +41 -0
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
- package/src/bmm-skills/3-solutioning/bmad-generate-project-context/SKILL.md +76 -1
- package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +41 -0
- package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
- package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +48 -43
- package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +90 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/SKILL.md +46 -7
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md +6 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/SKILL.md +85 -1
- package/src/bmm-skills/4-implementation/bmad-code-review/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-04-present.md +6 -0
- package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +296 -1
- package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +424 -1
- package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-dev-story/SKILL.md +480 -1
- package/src/bmm-skills/4-implementation/bmad-dev-story/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +171 -1
- package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/SKILL.md +106 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-05-present.md +6 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +6 -0
- package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1507 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +294 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/customize.toml +41 -0
- package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +292 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-status/customize.toml +41 -0
- package/src/bmm-skills/module.yaml +49 -0
- package/src/core-skills/bmad-advanced-elicitation/SKILL.md +7 -1
- package/src/core-skills/bmad-customize/SKILL.md +111 -0
- package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
- package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
- package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
- package/src/core-skills/bmad-party-mode/SKILL.md +13 -10
- package/src/core-skills/module-help.csv +1 -0
- package/src/core-skills/module.yaml +2 -0
- package/src/scripts/resolve_config.py +176 -0
- package/src/scripts/resolve_customization.py +230 -0
- package/tools/installer/commands/install.js +13 -0
- package/tools/installer/core/config.js +4 -1
- package/tools/installer/core/install-paths.js +11 -5
- package/tools/installer/core/installer.js +181 -94
- package/tools/installer/core/manifest-generator.js +339 -184
- package/tools/installer/core/manifest.js +86 -86
- package/tools/installer/fs-native.js +5 -0
- package/tools/installer/ide/platform-codes.yaml +6 -0
- package/tools/installer/modules/channel-plan.js +203 -0
- package/tools/installer/modules/channel-resolver.js +241 -0
- package/tools/installer/modules/community-manager.js +130 -23
- package/tools/installer/modules/custom-module-manager.js +160 -19
- package/tools/installer/modules/external-manager.js +235 -32
- package/tools/installer/modules/official-modules.js +58 -12
- package/tools/installer/modules/registry-client.js +139 -7
- package/tools/installer/modules/registry-fallback.yaml +8 -0
- package/tools/installer/modules/version-resolver.js +336 -0
- package/tools/installer/project-root.js +54 -0
- package/tools/installer/ui.js +561 -50
- package/tools/platform-codes.yaml +6 -0
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +0 -25
- package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +0 -51
- package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +0 -51
- package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +0 -52
- package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +0 -61
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +0 -35
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +0 -62
- package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +0 -61
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +0 -47
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +0 -32
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +0 -51
- package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +0 -39
- package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/4-implementation/bmad-code-review/workflow.md +0 -55
- package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +0 -267
- package/src/bmm-skills/4-implementation/bmad-create-story/workflow.md +0 -380
- package/src/bmm-skills/4-implementation/bmad-dev-story/workflow.md +0 -450
- package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/workflow.md +0 -136
- package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +0 -76
- package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/workflow.md +0 -263
- package/src/bmm-skills/4-implementation/bmad-sprint-status/workflow.md +0 -261
|
@@ -15,6 +15,11 @@ class OfficialModules {
|
|
|
15
15
|
// Tracked during interactive config collection so {directory_name}
|
|
16
16
|
// placeholder defaults can be resolved in buildQuestion().
|
|
17
17
|
this.currentProjectDir = null;
|
|
18
|
+
// Install-time channel flag state. Set by Config.build once, then used as
|
|
19
|
+
// the default for every findModuleSource/cloneExternalModule call so that
|
|
20
|
+
// pre-install config collection and the install step agree on which ref
|
|
21
|
+
// to clone.
|
|
22
|
+
this.channelOptions = options.channelOptions || null;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
/**
|
|
@@ -38,7 +43,7 @@ class OfficialModules {
|
|
|
38
43
|
* @returns {OfficialModules}
|
|
39
44
|
*/
|
|
40
45
|
static async build(config, paths) {
|
|
41
|
-
const instance = new OfficialModules();
|
|
46
|
+
const instance = new OfficialModules({ channelOptions: config.channelOptions });
|
|
42
47
|
|
|
43
48
|
// Pre-collected by UI or quickUpdate — store and load existing for path-change detection
|
|
44
49
|
if (config.moduleConfigs) {
|
|
@@ -196,6 +201,12 @@ class OfficialModules {
|
|
|
196
201
|
* @returns {string|null} Path to the module source or null if not found
|
|
197
202
|
*/
|
|
198
203
|
async findModuleSource(moduleCode, options = {}) {
|
|
204
|
+
// Inherit channelOptions from the install-scoped instance when the caller
|
|
205
|
+
// didn't pass one explicitly. Keeps pre-install config collection and the
|
|
206
|
+
// actual install step looking at the same git ref.
|
|
207
|
+
if (options.channelOptions === undefined && this.channelOptions) {
|
|
208
|
+
options = { ...options, channelOptions: this.channelOptions };
|
|
209
|
+
}
|
|
199
210
|
const projectRoot = getProjectRoot();
|
|
200
211
|
|
|
201
212
|
// Check for core module (directly under src/core-skills)
|
|
@@ -214,13 +225,13 @@ class OfficialModules {
|
|
|
214
225
|
}
|
|
215
226
|
}
|
|
216
227
|
|
|
217
|
-
// Check external official modules
|
|
228
|
+
// Check external official modules (pass channelOptions so channel plan applies)
|
|
218
229
|
const externalSource = await this.externalModuleManager.findExternalModuleSource(moduleCode, options);
|
|
219
230
|
if (externalSource) {
|
|
220
231
|
return externalSource;
|
|
221
232
|
}
|
|
222
233
|
|
|
223
|
-
// Check community modules
|
|
234
|
+
// Check community modules (pass channelOptions for --next/--pin overrides)
|
|
224
235
|
const { CommunityModuleManager } = require('./community-manager');
|
|
225
236
|
const communityMgr = new CommunityModuleManager();
|
|
226
237
|
const communitySource = await communityMgr.findModuleSource(moduleCode, options);
|
|
@@ -258,7 +269,10 @@ class OfficialModules {
|
|
|
258
269
|
return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
|
|
259
270
|
}
|
|
260
271
|
|
|
261
|
-
const sourcePath = await this.findModuleSource(moduleName, {
|
|
272
|
+
const sourcePath = await this.findModuleSource(moduleName, {
|
|
273
|
+
silent: options.silent,
|
|
274
|
+
channelOptions: options.channelOptions,
|
|
275
|
+
});
|
|
262
276
|
const targetPath = path.join(bmadDir, moduleName);
|
|
263
277
|
|
|
264
278
|
if (!sourcePath) {
|
|
@@ -281,11 +295,24 @@ class OfficialModules {
|
|
|
281
295
|
const manifestObj = new Manifest();
|
|
282
296
|
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
|
283
297
|
|
|
298
|
+
// Pick up channel resolution recorded by whichever manager did the clone.
|
|
299
|
+
const externalResolution = this.externalModuleManager.getResolution(moduleName);
|
|
300
|
+
let communityResolution = null;
|
|
301
|
+
if (!externalResolution) {
|
|
302
|
+
const { CommunityModuleManager } = require('./community-manager');
|
|
303
|
+
communityResolution = new CommunityModuleManager().getResolution(moduleName);
|
|
304
|
+
}
|
|
305
|
+
const resolution = externalResolution || communityResolution;
|
|
306
|
+
|
|
284
307
|
await manifestObj.addModule(bmadDir, moduleName, {
|
|
285
|
-
version: versionInfo.version,
|
|
308
|
+
version: resolution?.version || versionInfo.version,
|
|
286
309
|
source: versionInfo.source,
|
|
287
310
|
npmPackage: versionInfo.npmPackage,
|
|
288
311
|
repoUrl: versionInfo.repoUrl,
|
|
312
|
+
channel: resolution?.channel,
|
|
313
|
+
sha: resolution?.sha,
|
|
314
|
+
registryApprovedTag: communityResolution?.registryApprovedTag,
|
|
315
|
+
registryApprovedSha: communityResolution?.registryApprovedSha,
|
|
289
316
|
});
|
|
290
317
|
|
|
291
318
|
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
|
@@ -333,18 +360,37 @@ class OfficialModules {
|
|
|
333
360
|
await this.createModuleDirectories(resolved.code, bmadDir, options);
|
|
334
361
|
}
|
|
335
362
|
|
|
336
|
-
// Update manifest
|
|
363
|
+
// Update manifest. For custom modules, derive channel from the git ref:
|
|
364
|
+
// cloneRef present → pinned at that ref
|
|
365
|
+
// cloneRef absent → next (main HEAD)
|
|
366
|
+
// local path → no channel concept
|
|
337
367
|
const { Manifest } = require('../core/manifest');
|
|
338
368
|
const manifestObj = new Manifest();
|
|
339
369
|
|
|
340
|
-
|
|
341
|
-
|
|
370
|
+
const hasGitClone = !!resolved.repoUrl;
|
|
371
|
+
const manifestEntry = {
|
|
372
|
+
version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null),
|
|
342
373
|
source: 'custom',
|
|
343
374
|
npmPackage: null,
|
|
344
375
|
repoUrl: resolved.repoUrl || null,
|
|
345
|
-
}
|
|
376
|
+
};
|
|
377
|
+
if (hasGitClone) {
|
|
378
|
+
manifestEntry.channel = resolved.cloneRef ? 'pinned' : 'next';
|
|
379
|
+
if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
|
|
380
|
+
if (resolved.rawInput) manifestEntry.rawSource = resolved.rawInput;
|
|
381
|
+
}
|
|
382
|
+
if (resolved.localPath) manifestEntry.localPath = resolved.localPath;
|
|
383
|
+
await manifestObj.addModule(bmadDir, resolved.code, manifestEntry);
|
|
346
384
|
|
|
347
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
success: true,
|
|
387
|
+
module: resolved.code,
|
|
388
|
+
path: targetPath,
|
|
389
|
+
// Match the manifestEntry.version expression above so downstream summary
|
|
390
|
+
// lines show the cloned ref (tag or 'main') instead of the on-disk
|
|
391
|
+
// package.json version for git-backed custom installs.
|
|
392
|
+
versionInfo: { version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || '') },
|
|
393
|
+
};
|
|
348
394
|
}
|
|
349
395
|
|
|
350
396
|
/**
|
|
@@ -820,10 +866,10 @@ class OfficialModules {
|
|
|
820
866
|
let foundAny = false;
|
|
821
867
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
822
868
|
|
|
869
|
+
const nonModuleDirs = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom']);
|
|
823
870
|
for (const entry of entries) {
|
|
824
871
|
if (entry.isDirectory()) {
|
|
825
|
-
|
|
826
|
-
if (entry.name === '_config' || entry.name === '_memory') {
|
|
872
|
+
if (nonModuleDirs.has(entry.name)) {
|
|
827
873
|
continue;
|
|
828
874
|
}
|
|
829
875
|
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
const https = require('node:https');
|
|
2
2
|
const yaml = require('yaml');
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Build a rich Error from a non-2xx response. Includes the URL, the GitHub
|
|
6
|
+
* JSON error message (or a truncated body snippet), rate-limit reset time,
|
|
7
|
+
* and Retry-After — anything present that would help a user recover.
|
|
8
|
+
*/
|
|
9
|
+
function buildHttpError(url, res, body) {
|
|
10
|
+
const parts = [`HTTP ${res.statusCode} ${url}`];
|
|
11
|
+
|
|
12
|
+
if (body) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(body);
|
|
15
|
+
if (parsed.message) parts.push(parsed.message);
|
|
16
|
+
if (parsed.documentation_url) parts.push(`(see ${parsed.documentation_url})`);
|
|
17
|
+
} catch {
|
|
18
|
+
const snippet = body.slice(0, 200).trim();
|
|
19
|
+
if (snippet) parts.push(snippet);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const remaining = res.headers['x-ratelimit-remaining'];
|
|
24
|
+
const reset = res.headers['x-ratelimit-reset'];
|
|
25
|
+
if (remaining === '0' && reset) {
|
|
26
|
+
parts.push(`rate limit exhausted; resets at ${new Date(Number(reset) * 1000).toISOString()}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const retryAfter = res.headers['retry-after'];
|
|
30
|
+
if (retryAfter) parts.push(`retry after ${retryAfter}`);
|
|
31
|
+
|
|
32
|
+
return new Error(parts.join(' — '));
|
|
33
|
+
}
|
|
34
|
+
|
|
4
35
|
/**
|
|
5
36
|
* Shared HTTP client for fetching registry data from GitHub.
|
|
6
37
|
* Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
|
|
@@ -12,25 +43,31 @@ class RegistryClient {
|
|
|
12
43
|
|
|
13
44
|
/**
|
|
14
45
|
* Fetch a URL and return the response body as a string.
|
|
15
|
-
* Follows
|
|
46
|
+
* Follows up to 3 redirects (GitHub sometimes 301s).
|
|
16
47
|
* @param {string} url - URL to fetch
|
|
17
48
|
* @param {number} [timeout] - Timeout in ms (overrides default)
|
|
49
|
+
* @param {number} [maxRedirects=3] - Maximum redirects to follow
|
|
18
50
|
* @returns {Promise<string>} Response body
|
|
19
51
|
*/
|
|
20
|
-
fetch(url, timeout) {
|
|
52
|
+
fetch(url, timeout, maxRedirects = 3) {
|
|
21
53
|
const timeoutMs = timeout || this.timeout;
|
|
22
54
|
return new Promise((resolve, reject) => {
|
|
23
55
|
const req = https
|
|
24
56
|
.get(url, { timeout: timeoutMs }, (res) => {
|
|
25
57
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return
|
|
58
|
+
if (maxRedirects <= 0) {
|
|
59
|
+
return reject(new Error('Too many redirects'));
|
|
60
|
+
}
|
|
61
|
+
return this.fetch(res.headers.location, timeoutMs, maxRedirects - 1).then(resolve, reject);
|
|
30
62
|
}
|
|
31
63
|
let data = '';
|
|
32
64
|
res.on('data', (chunk) => (data += chunk));
|
|
33
|
-
res.on('end', () =>
|
|
65
|
+
res.on('end', () => {
|
|
66
|
+
if (res.statusCode !== 200) {
|
|
67
|
+
return reject(buildHttpError(url, res, data));
|
|
68
|
+
}
|
|
69
|
+
resolve(data);
|
|
70
|
+
});
|
|
34
71
|
})
|
|
35
72
|
.on('error', reject)
|
|
36
73
|
.on('timeout', () => {
|
|
@@ -50,6 +87,101 @@ class RegistryClient {
|
|
|
50
87
|
const content = await this.fetch(url, timeout);
|
|
51
88
|
return yaml.parse(content);
|
|
52
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fetch a file from a GitHub repo using the Contents API first,
|
|
93
|
+
* falling back to raw.githubusercontent.com if the API fails.
|
|
94
|
+
*
|
|
95
|
+
* The API endpoint (`api.github.com`) is tried first because corporate
|
|
96
|
+
* proxies commonly block `raw.githubusercontent.com` while allowing
|
|
97
|
+
* `api.github.com` under the "Software Development" category.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} owner - Repository owner (e.g., 'bmad-code-org')
|
|
100
|
+
* @param {string} repo - Repository name (e.g., 'bmad-plugins-marketplace')
|
|
101
|
+
* @param {string} filePath - Path within the repo (e.g., 'registry/official.yaml')
|
|
102
|
+
* @param {string} ref - Git ref (branch, tag, or SHA; e.g., 'main')
|
|
103
|
+
* @param {number} [timeout] - Timeout in ms (overrides default)
|
|
104
|
+
* @returns {Promise<string>} Raw file content
|
|
105
|
+
*/
|
|
106
|
+
async fetchGitHubFile(owner, repo, filePath, ref, timeout) {
|
|
107
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${ref}`;
|
|
108
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`;
|
|
109
|
+
|
|
110
|
+
// Try GitHub Contents API first (with raw content accept header)
|
|
111
|
+
try {
|
|
112
|
+
return await this._fetchWithHeaders(apiUrl, { Accept: 'application/vnd.github.raw+json' }, timeout);
|
|
113
|
+
} catch (apiError) {
|
|
114
|
+
// API failed — fall back to raw CDN
|
|
115
|
+
try {
|
|
116
|
+
return await this.fetch(rawUrl, timeout);
|
|
117
|
+
} catch (cdnError) {
|
|
118
|
+
throw new AggregateError([apiError, cdnError], `Both GitHub API and raw CDN failed for ${filePath}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fetch a file from GitHub and parse as YAML.
|
|
125
|
+
* @param {string} owner - Repository owner
|
|
126
|
+
* @param {string} repo - Repository name
|
|
127
|
+
* @param {string} filePath - Path within the repo
|
|
128
|
+
* @param {string} ref - Git ref
|
|
129
|
+
* @param {number} [timeout] - Timeout in ms
|
|
130
|
+
* @returns {Promise<Object>} Parsed YAML content
|
|
131
|
+
*/
|
|
132
|
+
async fetchGitHubYaml(owner, repo, filePath, ref, timeout) {
|
|
133
|
+
const content = await this.fetchGitHubFile(owner, repo, filePath, ref, timeout);
|
|
134
|
+
return yaml.parse(content);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch a URL with custom headers. Used for GitHub API requests.
|
|
139
|
+
* Follows up to 3 redirects.
|
|
140
|
+
* @param {string} url - URL to fetch
|
|
141
|
+
* @param {Object} headers - Request headers
|
|
142
|
+
* @param {number} [timeout] - Timeout in ms
|
|
143
|
+
* @param {number} [maxRedirects=3] - Maximum redirects to follow
|
|
144
|
+
* @returns {Promise<string>} Response body
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
_fetchWithHeaders(url, headers, timeout, maxRedirects = 3) {
|
|
148
|
+
const timeoutMs = timeout || this.timeout;
|
|
149
|
+
const parsed = new URL(url);
|
|
150
|
+
const options = {
|
|
151
|
+
hostname: parsed.hostname,
|
|
152
|
+
path: parsed.pathname + parsed.search,
|
|
153
|
+
timeout: timeoutMs,
|
|
154
|
+
headers: {
|
|
155
|
+
'User-Agent': 'bmad-installer',
|
|
156
|
+
...headers,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const req = https
|
|
162
|
+
.get(options, (res) => {
|
|
163
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
164
|
+
if (maxRedirects <= 0) {
|
|
165
|
+
return reject(new Error('Too many redirects'));
|
|
166
|
+
}
|
|
167
|
+
return this._fetchWithHeaders(res.headers.location, headers, timeoutMs, maxRedirects - 1).then(resolve, reject);
|
|
168
|
+
}
|
|
169
|
+
let data = '';
|
|
170
|
+
res.on('data', (chunk) => (data += chunk));
|
|
171
|
+
res.on('end', () => {
|
|
172
|
+
if (res.statusCode !== 200) {
|
|
173
|
+
return reject(buildHttpError(url, res, data));
|
|
174
|
+
}
|
|
175
|
+
resolve(data);
|
|
176
|
+
});
|
|
177
|
+
})
|
|
178
|
+
.on('error', reject)
|
|
179
|
+
.on('timeout', () => {
|
|
180
|
+
req.destroy();
|
|
181
|
+
reject(new Error('Request timed out'));
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
53
185
|
}
|
|
54
186
|
|
|
55
187
|
module.exports = { RegistryClient };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Fallback module registry — used only when the BMad Marketplace repo
|
|
2
2
|
# (bmad-code-org/bmad-plugins-marketplace) is unreachable.
|
|
3
3
|
# The remote registry/official.yaml is the source of truth.
|
|
4
|
+
#
|
|
5
|
+
# default_channel (optional) — the install channel when the user does not
|
|
6
|
+
# override with --channel/--pin/--next. Valid values: stable | next.
|
|
7
|
+
# Omit to inherit the installer's hardcoded default (stable).
|
|
4
8
|
|
|
5
9
|
modules:
|
|
6
10
|
bmad-builder:
|
|
@@ -12,6 +16,7 @@ modules:
|
|
|
12
16
|
defaultSelected: false
|
|
13
17
|
type: bmad-org
|
|
14
18
|
npmPackage: bmad-builder
|
|
19
|
+
default_channel: stable
|
|
15
20
|
|
|
16
21
|
bmad-creative-intelligence-suite:
|
|
17
22
|
url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite
|
|
@@ -22,6 +27,7 @@ modules:
|
|
|
22
27
|
defaultSelected: false
|
|
23
28
|
type: bmad-org
|
|
24
29
|
npmPackage: bmad-creative-intelligence-suite
|
|
30
|
+
default_channel: stable
|
|
25
31
|
|
|
26
32
|
bmad-game-dev-studio:
|
|
27
33
|
url: https://github.com/bmad-code-org/bmad-module-game-dev-studio.git
|
|
@@ -32,6 +38,7 @@ modules:
|
|
|
32
38
|
defaultSelected: false
|
|
33
39
|
type: bmad-org
|
|
34
40
|
npmPackage: bmad-game-dev-studio
|
|
41
|
+
default_channel: stable
|
|
35
42
|
|
|
36
43
|
bmad-method-test-architecture-enterprise:
|
|
37
44
|
url: https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise
|
|
@@ -42,3 +49,4 @@ modules:
|
|
|
42
49
|
defaultSelected: false
|
|
43
50
|
type: bmad-org
|
|
44
51
|
npmPackage: bmad-method-test-architecture-enterprise
|
|
52
|
+
default_channel: stable
|
|
@@ -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
|
+
};
|