bmad-method 6.3.1-next.9 → 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/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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const https = require('node:https');
|
|
2
|
+
const semver = require('semver');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Channel resolver for external and community modules.
|
|
6
|
+
*
|
|
7
|
+
* A "channel" is the resolution strategy that decides which ref of a module
|
|
8
|
+
* to clone when no explicit version is supplied:
|
|
9
|
+
* - stable: highest pure-semver git tag (excludes -alpha/-beta/-rc)
|
|
10
|
+
* - next: main branch HEAD
|
|
11
|
+
* - pinned: an explicit user-supplied tag
|
|
12
|
+
*
|
|
13
|
+
* This module is pure (no prompts, no git, no filesystem). It only talks to
|
|
14
|
+
* the GitHub tags API and performs semver math. Clone logic lives in the
|
|
15
|
+
* module managers that call resolveChannel().
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const GITHUB_API_BASE = 'https://api.github.com';
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
20
|
+
const USER_AGENT = 'bmad-method-installer';
|
|
21
|
+
|
|
22
|
+
// Per-process cache: { 'owner/repo' => string[] sorted desc } of pure-semver tags.
|
|
23
|
+
const tagCache = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a GitHub repo URL into { owner, repo }. Returns null if the URL is
|
|
27
|
+
* not a GitHub URL the resolver can handle.
|
|
28
|
+
*/
|
|
29
|
+
function parseGitHubRepo(url) {
|
|
30
|
+
if (!url || typeof url !== 'string') return null;
|
|
31
|
+
const trimmed = url
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/\.git$/, '')
|
|
34
|
+
.replace(/\/$/, '');
|
|
35
|
+
|
|
36
|
+
// https://github.com/owner/repo
|
|
37
|
+
const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?$/i);
|
|
38
|
+
if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
39
|
+
|
|
40
|
+
// git@github.com:owner/repo
|
|
41
|
+
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/]+)$/i);
|
|
42
|
+
if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fetchJson(url, { timeout = DEFAULT_TIMEOUT_MS } = {}) {
|
|
48
|
+
const headers = {
|
|
49
|
+
'User-Agent': USER_AGENT,
|
|
50
|
+
Accept: 'application/vnd.github+json',
|
|
51
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
52
|
+
};
|
|
53
|
+
if (process.env.GITHUB_TOKEN) {
|
|
54
|
+
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const req = https.get(url, { headers, timeout }, (res) => {
|
|
59
|
+
let body = '';
|
|
60
|
+
res.on('data', (chunk) => (body += chunk));
|
|
61
|
+
res.on('end', () => {
|
|
62
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
63
|
+
const err = new Error(`GitHub API ${res.statusCode} for ${url}: ${body.slice(0, 200)}`);
|
|
64
|
+
err.statusCode = res.statusCode;
|
|
65
|
+
return reject(err);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
resolve(JSON.parse(body));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
reject(new Error(`Failed to parse GitHub response: ${error.message}`));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
req.on('error', reject);
|
|
75
|
+
req.on('timeout', () => {
|
|
76
|
+
req.destroy();
|
|
77
|
+
reject(new Error(`GitHub API request timed out: ${url}`));
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Strip a leading 'v' and return a valid semver string, or null if the tag
|
|
84
|
+
* is not valid semver or is a prerelease (contains -alpha/-beta/-rc/etc.).
|
|
85
|
+
*/
|
|
86
|
+
function normalizeStableTag(tagName) {
|
|
87
|
+
if (typeof tagName !== 'string') return null;
|
|
88
|
+
const stripped = tagName.startsWith('v') ? tagName.slice(1) : tagName;
|
|
89
|
+
const valid = semver.valid(stripped);
|
|
90
|
+
if (!valid) return null;
|
|
91
|
+
// Exclude prereleases. semver.prerelease returns null for pure releases.
|
|
92
|
+
if (semver.prerelease(valid)) return null;
|
|
93
|
+
return valid;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fetch pure-semver tags (highest first) from a GitHub repo.
|
|
98
|
+
* Cached per-process per owner/repo.
|
|
99
|
+
*
|
|
100
|
+
* @returns {Promise<Array<{tag: string, version: string}>>}
|
|
101
|
+
* tag is the original ref name (e.g. "v1.7.0"), version is the cleaned
|
|
102
|
+
* semver (e.g. "1.7.0").
|
|
103
|
+
*/
|
|
104
|
+
async function fetchStableTags(owner, repo, { timeout } = {}) {
|
|
105
|
+
const cacheKey = `${owner}/${repo}`;
|
|
106
|
+
if (tagCache.has(cacheKey)) return tagCache.get(cacheKey);
|
|
107
|
+
|
|
108
|
+
// GitHub returns up to 100 tags per page; one page is plenty for our modules.
|
|
109
|
+
const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/tags?per_page=100`;
|
|
110
|
+
const raw = await fetchJson(url, { timeout });
|
|
111
|
+
if (!Array.isArray(raw)) {
|
|
112
|
+
throw new TypeError(`Unexpected response from ${url}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const stable = [];
|
|
116
|
+
for (const entry of raw) {
|
|
117
|
+
const version = normalizeStableTag(entry?.name);
|
|
118
|
+
if (version) stable.push({ tag: entry.name, version });
|
|
119
|
+
}
|
|
120
|
+
stable.sort((a, b) => semver.rcompare(a.version, b.version));
|
|
121
|
+
|
|
122
|
+
tagCache.set(cacheKey, stable);
|
|
123
|
+
return stable;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve a channel plan for a single module into a git-clonable ref.
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} args
|
|
130
|
+
* @param {'stable'|'next'|'pinned'} args.channel
|
|
131
|
+
* @param {string} [args.pin] - Required when channel === 'pinned'
|
|
132
|
+
* @param {string} args.repoUrl - Module's git URL (for tag lookup)
|
|
133
|
+
* @returns {Promise<{channel, ref, version}>} where
|
|
134
|
+
* ref: the git ref to pass to `git clone --branch`, or null for HEAD (next)
|
|
135
|
+
* version: the resolved version string (tag name for stable/pinned, 'main' for next)
|
|
136
|
+
*
|
|
137
|
+
* Throws on:
|
|
138
|
+
* - pinned without a pin value
|
|
139
|
+
* - stable with no GitHub repo parseable from the URL (pass through to caller to fall back)
|
|
140
|
+
*
|
|
141
|
+
* Falls back to next-channel semantics and sets resolvedFallback=true when
|
|
142
|
+
* stable resolution turns up no tags.
|
|
143
|
+
*/
|
|
144
|
+
async function resolveChannel({ channel, pin, repoUrl, timeout }) {
|
|
145
|
+
if (channel === 'pinned') {
|
|
146
|
+
if (!pin) throw new Error('resolveChannel: pinned channel requires a pin value');
|
|
147
|
+
return { channel: 'pinned', ref: pin, version: pin, resolvedFallback: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (channel === 'next') {
|
|
151
|
+
return { channel: 'next', ref: null, version: 'main', resolvedFallback: false };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (channel === 'stable') {
|
|
155
|
+
const parsed = parseGitHubRepo(repoUrl);
|
|
156
|
+
if (!parsed) {
|
|
157
|
+
// No GitHub URL — caller must handle by falling back to next.
|
|
158
|
+
return { channel: 'next', ref: null, version: 'main', resolvedFallback: true, reason: 'not-a-github-url' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const tags = await fetchStableTags(parsed.owner, parsed.repo, { timeout });
|
|
163
|
+
if (tags.length === 0) {
|
|
164
|
+
return { channel: 'next', ref: null, version: 'main', resolvedFallback: true, reason: 'no-stable-tags' };
|
|
165
|
+
}
|
|
166
|
+
const top = tags[0];
|
|
167
|
+
return { channel: 'stable', ref: top.tag, version: top.tag, resolvedFallback: false };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// Propagate the error; callers decide whether to fall back or abort.
|
|
170
|
+
error.message = `Failed to resolve stable channel for ${parsed.owner}/${parsed.repo}: ${error.message}`;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new Error(`resolveChannel: unknown channel '${channel}'`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Verify that a specific tag exists in a GitHub repo. Used to validate
|
|
180
|
+
* --pin values before the user sits through a long clone that then fails.
|
|
181
|
+
*/
|
|
182
|
+
async function tagExists(owner, repo, tagName, { timeout } = {}) {
|
|
183
|
+
const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/git/refs/tags/${encodeURIComponent(tagName)}`;
|
|
184
|
+
try {
|
|
185
|
+
await fetchJson(url, { timeout });
|
|
186
|
+
return true;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.statusCode === 404) return false;
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Classify the semver delta between two versions.
|
|
195
|
+
* - 'none' → same version (or downgrade; treated same)
|
|
196
|
+
* - 'patch' → same major.minor, higher patch
|
|
197
|
+
* - 'minor' → same major, higher minor
|
|
198
|
+
* - 'major' → different major
|
|
199
|
+
* - 'unknown' → either version is not valid semver; caller should treat as major
|
|
200
|
+
*/
|
|
201
|
+
function classifyUpgrade(currentVersion, newVersion) {
|
|
202
|
+
const current = semver.valid(semver.coerce(currentVersion));
|
|
203
|
+
const next = semver.valid(semver.coerce(newVersion));
|
|
204
|
+
if (!current || !next) return 'unknown';
|
|
205
|
+
if (semver.lte(next, current)) return 'none';
|
|
206
|
+
const diff = semver.diff(current, next);
|
|
207
|
+
if (diff === 'patch') return 'patch';
|
|
208
|
+
if (diff === 'minor' || diff === 'preminor') return 'minor';
|
|
209
|
+
if (diff === 'major' || diff === 'premajor') return 'major';
|
|
210
|
+
// prepatch, prerelease — treat conservatively as minor (prereleases shouldn't
|
|
211
|
+
// normally surface here since stable channel filters them out).
|
|
212
|
+
return 'minor';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build the GitHub release notes URL for a resolved tag.
|
|
217
|
+
* Returns null if the repo URL isn't a GitHub URL.
|
|
218
|
+
*/
|
|
219
|
+
function releaseNotesUrl(repoUrl, tag) {
|
|
220
|
+
const parsed = parseGitHubRepo(repoUrl);
|
|
221
|
+
if (!parsed || !tag) return null;
|
|
222
|
+
return `https://github.com/${parsed.owner}/${parsed.repo}/releases/tag/${encodeURIComponent(tag)}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Test-only: clear the per-process tag cache.
|
|
227
|
+
*/
|
|
228
|
+
function _clearTagCache() {
|
|
229
|
+
tagCache.clear();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
parseGitHubRepo,
|
|
234
|
+
fetchStableTags,
|
|
235
|
+
resolveChannel,
|
|
236
|
+
tagExists,
|
|
237
|
+
classifyUpgrade,
|
|
238
|
+
releaseNotesUrl,
|
|
239
|
+
normalizeStableTag,
|
|
240
|
+
_clearTagCache,
|
|
241
|
+
};
|
|
@@ -4,10 +4,12 @@ const path = require('node:path');
|
|
|
4
4
|
const { execSync } = require('node:child_process');
|
|
5
5
|
const prompts = require('../prompts');
|
|
6
6
|
const { RegistryClient } = require('./registry-client');
|
|
7
|
+
const { decideChannelForModule } = require('./channel-plan');
|
|
8
|
+
const { parseGitHubRepo, tagExists } = require('./channel-resolver');
|
|
7
9
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const MARKETPLACE_OWNER = 'bmad-code-org';
|
|
11
|
+
const MARKETPLACE_REPO = 'bmad-plugins-marketplace';
|
|
12
|
+
const MARKETPLACE_REF = 'main';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Manages community modules from the BMad marketplace registry.
|
|
@@ -15,13 +17,29 @@ const CATEGORIES_URL = `${MARKETPLACE_BASE}/categories.yaml`;
|
|
|
15
17
|
* Returns empty results when the registry is unreachable.
|
|
16
18
|
* Community modules are pinned to approved SHA when set; uses HEAD otherwise.
|
|
17
19
|
*/
|
|
20
|
+
function quoteShellRef(ref) {
|
|
21
|
+
if (typeof ref !== 'string' || !/^[\w.\-+/]+$/.test(ref)) {
|
|
22
|
+
throw new Error(`Unsafe ref name: ${JSON.stringify(ref)}`);
|
|
23
|
+
}
|
|
24
|
+
return `"${ref}"`;
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
class CommunityModuleManager {
|
|
28
|
+
// moduleCode → { channel, version, sha, registryApprovedTag, registryApprovedSha, repoUrl, bypassedCurator }
|
|
29
|
+
// Shared across all instances; the manifest writer often uses a fresh instance.
|
|
30
|
+
static _resolutions = new Map();
|
|
31
|
+
|
|
19
32
|
constructor() {
|
|
20
33
|
this._client = new RegistryClient();
|
|
21
34
|
this._cachedIndex = null;
|
|
22
35
|
this._cachedCategories = null;
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
/** Get the most recent channel resolution for a community module. */
|
|
39
|
+
getResolution(moduleCode) {
|
|
40
|
+
return CommunityModuleManager._resolutions.get(moduleCode) || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
// ─── Data Loading ──────────────────────────────────────────────────────────
|
|
26
44
|
|
|
27
45
|
/**
|
|
@@ -33,7 +51,12 @@ class CommunityModuleManager {
|
|
|
33
51
|
if (this._cachedIndex) return this._cachedIndex;
|
|
34
52
|
|
|
35
53
|
try {
|
|
36
|
-
const config = await this._client.
|
|
54
|
+
const config = await this._client.fetchGitHubYaml(
|
|
55
|
+
MARKETPLACE_OWNER,
|
|
56
|
+
MARKETPLACE_REPO,
|
|
57
|
+
'registry/community-index.yaml',
|
|
58
|
+
MARKETPLACE_REF,
|
|
59
|
+
);
|
|
37
60
|
if (config?.modules?.length) {
|
|
38
61
|
this._cachedIndex = config;
|
|
39
62
|
return config;
|
|
@@ -54,7 +77,7 @@ class CommunityModuleManager {
|
|
|
54
77
|
if (this._cachedCategories) return this._cachedCategories;
|
|
55
78
|
|
|
56
79
|
try {
|
|
57
|
-
const config = await this._client.
|
|
80
|
+
const config = await this._client.fetchGitHubYaml(MARKETPLACE_OWNER, MARKETPLACE_REPO, 'categories.yaml', MARKETPLACE_REF);
|
|
58
81
|
if (config?.categories) {
|
|
59
82
|
this._cachedCategories = config;
|
|
60
83
|
return config;
|
|
@@ -191,12 +214,49 @@ class CommunityModuleManager {
|
|
|
191
214
|
return await prompts.spinner();
|
|
192
215
|
};
|
|
193
216
|
|
|
194
|
-
|
|
217
|
+
// ─── Resolve channel plan ──────────────────────────────────────────────
|
|
218
|
+
// Default community behavior (stable channel) honors the curator's
|
|
219
|
+
// approved SHA. --next=CODE and --pin CODE=TAG override the curator; we
|
|
220
|
+
// warn the user before bypassing the approved version.
|
|
221
|
+
const planEntry = decideChannelForModule({
|
|
222
|
+
code: moduleCode,
|
|
223
|
+
channelOptions: options.channelOptions,
|
|
224
|
+
registryDefault: 'stable',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const approvedSha = moduleInfo.approvedSha;
|
|
228
|
+
const approvedTag = moduleInfo.approvedTag;
|
|
229
|
+
|
|
230
|
+
let bypassedCurator = false;
|
|
231
|
+
if (planEntry.channel !== 'stable') {
|
|
232
|
+
bypassedCurator = true;
|
|
233
|
+
if (!silent) {
|
|
234
|
+
const approvedLabel = approvedTag || approvedSha || 'curator-approved version';
|
|
235
|
+
await prompts.log.warn(
|
|
236
|
+
`WARNING: Installing '${moduleCode}' from ${
|
|
237
|
+
planEntry.channel === 'pinned' ? `tag ${planEntry.pin}` : 'main HEAD'
|
|
238
|
+
} bypasses the curator-approved ${approvedLabel}. Proceed only if you trust this source.`,
|
|
239
|
+
);
|
|
240
|
+
if (!options.channelOptions?.acceptBypass) {
|
|
241
|
+
const proceed = await prompts.confirm({
|
|
242
|
+
message: `Continue installing '${moduleCode}' with curator bypass?`,
|
|
243
|
+
default: false,
|
|
244
|
+
});
|
|
245
|
+
if (!proceed) {
|
|
246
|
+
throw new Error(`Install of community module '${moduleCode}' cancelled by user.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
let needsDependencyInstall = false;
|
|
196
253
|
let wasNewClone = false;
|
|
197
254
|
|
|
198
255
|
if (await fs.pathExists(moduleCacheDir)) {
|
|
199
|
-
// Already cloned
|
|
256
|
+
// Already cloned — refresh to the correct ref for the resolved channel.
|
|
257
|
+
// A pinned install must not reset to origin/HEAD (it would silently drift
|
|
258
|
+
// to main on every re-install). Stable + approvedSha is handled below
|
|
259
|
+
// by the curator-SHA checkout logic.
|
|
200
260
|
const fetchSpinner = await createSpinner();
|
|
201
261
|
fetchSpinner.start(`Checking ${moduleInfo.displayName}...`);
|
|
202
262
|
try {
|
|
@@ -206,10 +266,24 @@ class CommunityModuleManager {
|
|
|
206
266
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
207
267
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
208
268
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
269
|
+
if (planEntry.channel === 'pinned') {
|
|
270
|
+
// Fetch the pin tag specifically and check it out.
|
|
271
|
+
execSync(`git fetch --depth 1 origin ${quoteShellRef(planEntry.pin)} --no-tags`, {
|
|
272
|
+
cwd: moduleCacheDir,
|
|
273
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
274
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
275
|
+
});
|
|
276
|
+
execSync('git checkout --quiet FETCH_HEAD', {
|
|
277
|
+
cwd: moduleCacheDir,
|
|
278
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
279
|
+
});
|
|
280
|
+
} else {
|
|
281
|
+
// stable (approvedSha path re-checks out below) and next: track main.
|
|
282
|
+
execSync('git reset --hard origin/HEAD', {
|
|
283
|
+
cwd: moduleCacheDir,
|
|
284
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
213
287
|
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
|
214
288
|
if (currentRef !== newRef) needsDependencyInstall = true;
|
|
215
289
|
fetchSpinner.stop(`Verified ${moduleInfo.displayName}`);
|
|
@@ -226,10 +300,17 @@ class CommunityModuleManager {
|
|
|
226
300
|
const fetchSpinner = await createSpinner();
|
|
227
301
|
fetchSpinner.start(`Fetching ${moduleInfo.displayName}...`);
|
|
228
302
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
303
|
+
if (planEntry.channel === 'pinned') {
|
|
304
|
+
execSync(`git clone --depth 1 --branch ${quoteShellRef(planEntry.pin)} "${moduleInfo.url}" "${moduleCacheDir}"`, {
|
|
305
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
306
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
307
|
+
});
|
|
308
|
+
} else {
|
|
309
|
+
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
|
|
310
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
311
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
233
314
|
fetchSpinner.stop(`Fetched ${moduleInfo.displayName}`);
|
|
234
315
|
needsDependencyInstall = true;
|
|
235
316
|
} catch (error) {
|
|
@@ -238,18 +319,19 @@ class CommunityModuleManager {
|
|
|
238
319
|
}
|
|
239
320
|
}
|
|
240
321
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
322
|
+
// ─── Check out the resolved ref per channel ──────────────────────────
|
|
323
|
+
if (planEntry.channel === 'stable' && approvedSha) {
|
|
324
|
+
// Default path: pin to the curator-approved SHA. Refuse install if the SHA
|
|
325
|
+
// is unreachable (tag may have been deleted or rewritten) — security requirement.
|
|
244
326
|
const headSha = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
|
245
|
-
if (headSha !==
|
|
327
|
+
if (headSha !== approvedSha) {
|
|
246
328
|
try {
|
|
247
|
-
execSync(`git fetch --depth 1 origin ${
|
|
329
|
+
execSync(`git fetch --depth 1 origin ${quoteShellRef(approvedSha)}`, {
|
|
248
330
|
cwd: moduleCacheDir,
|
|
249
331
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
250
332
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
251
333
|
});
|
|
252
|
-
execSync(`git checkout ${
|
|
334
|
+
execSync(`git checkout ${quoteShellRef(approvedSha)}`, {
|
|
253
335
|
cwd: moduleCacheDir,
|
|
254
336
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
255
337
|
});
|
|
@@ -257,12 +339,37 @@ class CommunityModuleManager {
|
|
|
257
339
|
} catch {
|
|
258
340
|
await fs.remove(moduleCacheDir);
|
|
259
341
|
throw new Error(
|
|
260
|
-
`Community module '${moduleCode}' could not be pinned to its approved commit (${
|
|
261
|
-
`Installation refused for security. The module registry entry may need updating
|
|
342
|
+
`Community module '${moduleCode}' could not be pinned to its approved commit (${approvedSha}). ` +
|
|
343
|
+
`Installation refused for security. The module registry entry may need updating, ` +
|
|
344
|
+
`or use --next=${moduleCode} / --pin ${moduleCode}=<tag> to explicitly bypass.`,
|
|
262
345
|
);
|
|
263
346
|
}
|
|
264
347
|
}
|
|
348
|
+
} else if (planEntry.channel === 'stable' && !approvedSha) {
|
|
349
|
+
// Registry data gap: tag or SHA missing. Warn but proceed at HEAD (pre-existing behavior).
|
|
350
|
+
if (!silent) {
|
|
351
|
+
await prompts.log.warn(`Community module '${moduleCode}' has no curator-approved SHA in the registry; installing from main HEAD.`);
|
|
352
|
+
}
|
|
353
|
+
} else if (planEntry.channel === 'pinned') {
|
|
354
|
+
// We cloned the tag directly above (via --branch), but ensure HEAD matches.
|
|
355
|
+
// No additional checkout needed.
|
|
265
356
|
}
|
|
357
|
+
// else: 'next' channel — already at origin/HEAD from the fetch/reset above.
|
|
358
|
+
|
|
359
|
+
// Record the resolution so the manifest writer can pick up channel/version/sha.
|
|
360
|
+
const installedSha = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
|
361
|
+
const recordedVersion =
|
|
362
|
+
planEntry.channel === 'pinned' ? planEntry.pin : planEntry.channel === 'next' ? 'main' : approvedTag || installedSha.slice(0, 7);
|
|
363
|
+
CommunityModuleManager._resolutions.set(moduleCode, {
|
|
364
|
+
channel: planEntry.channel,
|
|
365
|
+
version: recordedVersion,
|
|
366
|
+
sha: installedSha,
|
|
367
|
+
registryApprovedTag: approvedTag || null,
|
|
368
|
+
registryApprovedSha: approvedSha || null,
|
|
369
|
+
repoUrl: moduleInfo.url,
|
|
370
|
+
bypassedCurator,
|
|
371
|
+
planSource: planEntry.source,
|
|
372
|
+
});
|
|
266
373
|
|
|
267
374
|
// Install dependencies if needed
|
|
268
375
|
const packageJsonPath = path.join(moduleCacheDir, 'package.json');
|