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
|
@@ -4,6 +4,13 @@ const path = require('node:path');
|
|
|
4
4
|
const { execSync } = require('node:child_process');
|
|
5
5
|
const prompts = require('../prompts');
|
|
6
6
|
|
|
7
|
+
function quoteCustomRef(ref) {
|
|
8
|
+
if (typeof ref !== 'string' || !/^[\w.\-+/]+$/.test(ref)) {
|
|
9
|
+
throw new Error(`Unsafe ref name: ${JSON.stringify(ref)}`);
|
|
10
|
+
}
|
|
11
|
+
return `"${ref}"`;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Manages custom modules installed from user-provided sources.
|
|
9
16
|
* Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted) and local file paths.
|
|
@@ -38,8 +45,8 @@ class CustomModuleManager {
|
|
|
38
45
|
};
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
48
|
+
const trimmedRaw = input.trim();
|
|
49
|
+
if (!trimmedRaw) {
|
|
43
50
|
return {
|
|
44
51
|
type: null,
|
|
45
52
|
cloneUrl: null,
|
|
@@ -52,8 +59,53 @@ class CustomModuleManager {
|
|
|
52
59
|
};
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
// Extract optional @<tag-or-branch> suffix from the end of the input.
|
|
63
|
+
// Semver-valid characters: letters, digits, dot, hyphen, underscore, plus, slash.
|
|
64
|
+
// Raw commit SHAs are NOT supported here — `git clone --branch` can't take
|
|
65
|
+
// them; use --pin at the module level or check out the SHA manually.
|
|
66
|
+
// Only strip when the tail looks like a ref, so we don't disturb
|
|
67
|
+
// URLs without a version spec or the SSH protocol's `git@host:...` prefix.
|
|
68
|
+
let trimmed = trimmedRaw;
|
|
69
|
+
let versionSuffix = null;
|
|
70
|
+
const lastAt = trimmedRaw.lastIndexOf('@');
|
|
71
|
+
// Skip if @ is part of git@github.com:... (first char cannot be stripped as version)
|
|
72
|
+
// and skip if @ appears before the path rather than after a ref-shaped tail.
|
|
73
|
+
if (lastAt > 0) {
|
|
74
|
+
const candidate = trimmedRaw.slice(lastAt + 1);
|
|
75
|
+
const before = trimmedRaw.slice(0, lastAt);
|
|
76
|
+
// candidate must be ref-shaped and must not itself look like a URL / SSH host
|
|
77
|
+
if (/^[\w.\-+/]+$/.test(candidate) && !candidate.includes(':')) {
|
|
78
|
+
// Avoid consuming the @ in `git@host:owner/repo` — `before` wouldn't end with a path separator
|
|
79
|
+
// in that case. Require that the @ comes after the host/path, not inside the auth segment.
|
|
80
|
+
// Rule: the @ is a version suffix only if `before` looks like a complete URL or local path.
|
|
81
|
+
const beforeLooksLikeRepo =
|
|
82
|
+
before.startsWith('/') ||
|
|
83
|
+
before.startsWith('./') ||
|
|
84
|
+
before.startsWith('../') ||
|
|
85
|
+
before.startsWith('~') ||
|
|
86
|
+
/^https?:\/\//i.test(before) ||
|
|
87
|
+
/^git@[^:]+:.+/.test(before);
|
|
88
|
+
if (beforeLooksLikeRepo) {
|
|
89
|
+
versionSuffix = candidate;
|
|
90
|
+
trimmed = before;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
55
95
|
// Local path detection: starts with /, ./, ../, or ~
|
|
56
96
|
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../') || trimmed.startsWith('~')) {
|
|
97
|
+
if (versionSuffix) {
|
|
98
|
+
return {
|
|
99
|
+
type: 'local',
|
|
100
|
+
cloneUrl: null,
|
|
101
|
+
subdir: null,
|
|
102
|
+
localPath: null,
|
|
103
|
+
cacheKey: null,
|
|
104
|
+
displayName: null,
|
|
105
|
+
isValid: false,
|
|
106
|
+
error: 'Local paths do not support @version suffixes',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
57
109
|
return this._parseLocalPath(trimmed);
|
|
58
110
|
}
|
|
59
111
|
|
|
@@ -66,6 +118,8 @@ class CustomModuleManager {
|
|
|
66
118
|
cloneUrl: trimmed,
|
|
67
119
|
subdir: null,
|
|
68
120
|
localPath: null,
|
|
121
|
+
version: versionSuffix || null,
|
|
122
|
+
rawInput: trimmedRaw,
|
|
69
123
|
cacheKey: `${host}/${owner}/${repo}`,
|
|
70
124
|
displayName: `${owner}/${repo}`,
|
|
71
125
|
isValid: true,
|
|
@@ -79,29 +133,47 @@ class CustomModuleManager {
|
|
|
79
133
|
const [, host, owner, repo, remainder] = httpsMatch;
|
|
80
134
|
const cloneUrl = `https://${host}/${owner}/${repo}`;
|
|
81
135
|
let subdir = null;
|
|
136
|
+
let urlRef = null; // branch/tag extracted from /tree/<ref>/subdir
|
|
82
137
|
|
|
83
138
|
if (remainder) {
|
|
84
139
|
// Extract subdir from deep path patterns used by various Git hosts
|
|
85
140
|
const deepPathPatterns = [
|
|
86
|
-
/^\/(?:-\/)?tree\/[^/]
|
|
87
|
-
/^\/(?:-\/)?blob\/[^/]
|
|
88
|
-
/^\/src\/[^/]
|
|
141
|
+
{ regex: /^\/(?:-\/)?tree\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // GitHub, GitLab
|
|
142
|
+
{ regex: /^\/(?:-\/)?blob\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 },
|
|
143
|
+
{ regex: /^\/src\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // Gitea/Forgejo
|
|
89
144
|
];
|
|
145
|
+
// Also match `/tree/<ref>` with no subdir
|
|
146
|
+
const refOnlyPatterns = [/^\/(?:-\/)?tree\/([^/]+?)\/?$/, /^\/(?:-\/)?blob\/([^/]+?)\/?$/, /^\/src\/([^/]+?)\/?$/];
|
|
90
147
|
|
|
91
|
-
for (const
|
|
92
|
-
const match = remainder.match(
|
|
148
|
+
for (const p of deepPathPatterns) {
|
|
149
|
+
const match = remainder.match(p.regex);
|
|
93
150
|
if (match) {
|
|
94
|
-
|
|
151
|
+
urlRef = match[p.refIdx];
|
|
152
|
+
subdir = match[p.pathIdx].replace(/\/$/, '');
|
|
95
153
|
break;
|
|
96
154
|
}
|
|
97
155
|
}
|
|
156
|
+
if (!subdir) {
|
|
157
|
+
for (const r of refOnlyPatterns) {
|
|
158
|
+
const match = remainder.match(r);
|
|
159
|
+
if (match) {
|
|
160
|
+
urlRef = match[1];
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
98
165
|
}
|
|
99
166
|
|
|
167
|
+
// Precedence: explicit @version suffix > URL /tree/<ref> path segment.
|
|
168
|
+
const version = versionSuffix || urlRef || null;
|
|
169
|
+
|
|
100
170
|
return {
|
|
101
171
|
type: 'url',
|
|
102
172
|
cloneUrl,
|
|
103
173
|
subdir,
|
|
104
174
|
localPath: null,
|
|
175
|
+
version,
|
|
176
|
+
rawInput: trimmedRaw,
|
|
105
177
|
cacheKey: `${host}/${owner}/${repo}`,
|
|
106
178
|
displayName: `${owner}/${repo}`,
|
|
107
179
|
isValid: true,
|
|
@@ -255,6 +327,10 @@ class CustomModuleManager {
|
|
|
255
327
|
const silent = options.silent || false;
|
|
256
328
|
const displayName = parsed.displayName;
|
|
257
329
|
|
|
330
|
+
// Pin override: --pin CODE=TAG resolved at module-selection time overrides
|
|
331
|
+
// any @version suffix present in the URL.
|
|
332
|
+
const effectiveVersion = options.pinOverride || parsed.version || null;
|
|
333
|
+
|
|
258
334
|
await fs.ensureDir(path.dirname(repoCacheDir));
|
|
259
335
|
|
|
260
336
|
const createSpinner = async () => {
|
|
@@ -264,8 +340,23 @@ class CustomModuleManager {
|
|
|
264
340
|
return await prompts.spinner();
|
|
265
341
|
};
|
|
266
342
|
|
|
343
|
+
// If an existing cache exists but was cloned at a different version, re-clone.
|
|
344
|
+
// Tracked via .bmad-source.json's recorded version.
|
|
267
345
|
if (await fs.pathExists(repoCacheDir)) {
|
|
268
|
-
|
|
346
|
+
let cachedVersion = null;
|
|
347
|
+
try {
|
|
348
|
+
const existing = await fs.readJson(path.join(repoCacheDir, '.bmad-source.json'));
|
|
349
|
+
cachedVersion = existing?.version || null;
|
|
350
|
+
} catch {
|
|
351
|
+
// no metadata; treat as mismatched to be safe if a version was requested
|
|
352
|
+
}
|
|
353
|
+
if ((effectiveVersion || null) !== (cachedVersion || null)) {
|
|
354
|
+
await fs.remove(repoCacheDir);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (await fs.pathExists(repoCacheDir)) {
|
|
359
|
+
// Update existing clone (same version as before)
|
|
269
360
|
const fetchSpinner = await createSpinner();
|
|
270
361
|
fetchSpinner.start(`Updating ${displayName}...`);
|
|
271
362
|
try {
|
|
@@ -274,10 +365,25 @@ class CustomModuleManager {
|
|
|
274
365
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
275
366
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
276
367
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
368
|
+
if (effectiveVersion) {
|
|
369
|
+
// Fetch the ref as either a tag or a branch — `origin <ref>` works
|
|
370
|
+
// for both, whereas `origin tag <ref>` fails for branch refs parsed
|
|
371
|
+
// out of /tree/<branch>/... URLs.
|
|
372
|
+
execSync(`git fetch --depth 1 origin ${quoteCustomRef(effectiveVersion)} --no-tags`, {
|
|
373
|
+
cwd: repoCacheDir,
|
|
374
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
375
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
376
|
+
});
|
|
377
|
+
execSync(`git checkout --quiet FETCH_HEAD`, {
|
|
378
|
+
cwd: repoCacheDir,
|
|
379
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
380
|
+
});
|
|
381
|
+
} else {
|
|
382
|
+
execSync('git reset --hard origin/HEAD', {
|
|
383
|
+
cwd: repoCacheDir,
|
|
384
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
385
|
+
});
|
|
386
|
+
}
|
|
281
387
|
fetchSpinner.stop(`Updated ${displayName}`);
|
|
282
388
|
} catch {
|
|
283
389
|
fetchSpinner.error(`Update failed, re-downloading ${displayName}`);
|
|
@@ -287,25 +393,44 @@ class CustomModuleManager {
|
|
|
287
393
|
|
|
288
394
|
if (!(await fs.pathExists(repoCacheDir))) {
|
|
289
395
|
const fetchSpinner = await createSpinner();
|
|
290
|
-
fetchSpinner.start(`Cloning ${displayName}...`);
|
|
396
|
+
fetchSpinner.start(`Cloning ${displayName}${effectiveVersion ? ` @ ${effectiveVersion}` : ''}...`);
|
|
291
397
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
398
|
+
if (effectiveVersion) {
|
|
399
|
+
execSync(`git clone --depth 1 --branch ${quoteCustomRef(effectiveVersion)} "${parsed.cloneUrl}" "${repoCacheDir}"`, {
|
|
400
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
401
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
execSync(`git clone --depth 1 "${parsed.cloneUrl}" "${repoCacheDir}"`, {
|
|
405
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
406
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
407
|
+
});
|
|
408
|
+
}
|
|
296
409
|
fetchSpinner.stop(`Cloned ${displayName}`);
|
|
297
410
|
} catch (error_) {
|
|
298
411
|
fetchSpinner.error(`Failed to clone ${displayName}`);
|
|
299
|
-
|
|
412
|
+
const refSuffix = effectiveVersion ? `@${effectiveVersion}` : '';
|
|
413
|
+
throw new Error(`Failed to clone ${parsed.cloneUrl}${refSuffix}: ${error_.message}`);
|
|
300
414
|
}
|
|
301
415
|
}
|
|
302
416
|
|
|
417
|
+
// Record the resolved SHA for the manifest writer.
|
|
418
|
+
let resolvedSha = null;
|
|
419
|
+
try {
|
|
420
|
+
resolvedSha = execSync('git rev-parse HEAD', { cwd: repoCacheDir, stdio: 'pipe' }).toString().trim();
|
|
421
|
+
} catch {
|
|
422
|
+
// swallow — a non-git repo (local path) wouldn't reach here anyway
|
|
423
|
+
}
|
|
424
|
+
|
|
303
425
|
// Write source metadata for later URL reconstruction
|
|
304
426
|
const metadataPath = path.join(repoCacheDir, '.bmad-source.json');
|
|
305
427
|
await fs.writeJson(metadataPath, {
|
|
306
428
|
cloneUrl: parsed.cloneUrl,
|
|
307
429
|
cacheKey: parsed.cacheKey,
|
|
308
430
|
displayName: parsed.displayName,
|
|
431
|
+
version: effectiveVersion || null,
|
|
432
|
+
rawInput: parsed.rawInput || sourceInput,
|
|
433
|
+
sha: resolvedSha,
|
|
309
434
|
clonedAt: new Date().toISOString(),
|
|
310
435
|
});
|
|
311
436
|
|
|
@@ -346,10 +471,26 @@ class CustomModuleManager {
|
|
|
346
471
|
const resolver = new PluginResolver();
|
|
347
472
|
const resolved = await resolver.resolve(repoPath, plugin);
|
|
348
473
|
|
|
474
|
+
// Read clone metadata (written by cloneRepo) so we can pick up the
|
|
475
|
+
// resolved git ref + SHA for manifest recording.
|
|
476
|
+
let cloneMetadata = null;
|
|
477
|
+
if (sourceUrl) {
|
|
478
|
+
try {
|
|
479
|
+
cloneMetadata = await fs.readJson(path.join(repoPath, '.bmad-source.json'));
|
|
480
|
+
} catch {
|
|
481
|
+
// no metadata — local-source or legacy cache
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
349
485
|
// Stamp source info onto each resolved module for manifest tracking
|
|
350
486
|
for (const mod of resolved) {
|
|
351
487
|
if (sourceUrl) mod.repoUrl = sourceUrl;
|
|
352
488
|
if (localPath) mod.localPath = localPath;
|
|
489
|
+
if (cloneMetadata) {
|
|
490
|
+
mod.cloneRef = cloneMetadata.version || null;
|
|
491
|
+
mod.cloneSha = cloneMetadata.sha || null;
|
|
492
|
+
mod.rawInput = cloneMetadata.rawInput || null;
|
|
493
|
+
}
|
|
353
494
|
CustomModuleManager._resolutionCache.set(mod.code, mod);
|
|
354
495
|
}
|
|
355
496
|
|
|
@@ -5,8 +5,50 @@ const { execSync } = require('node:child_process');
|
|
|
5
5
|
const yaml = require('yaml');
|
|
6
6
|
const prompts = require('../prompts');
|
|
7
7
|
const { RegistryClient } = require('./registry-client');
|
|
8
|
+
const { resolveChannel, tagExists, parseGitHubRepo } = require('./channel-resolver');
|
|
9
|
+
const { decideChannelForModule } = require('./channel-plan');
|
|
8
10
|
|
|
9
|
-
const
|
|
11
|
+
const VALID_CHANNELS = new Set(['stable', 'next', 'pinned']);
|
|
12
|
+
|
|
13
|
+
function normalizeChannelName(raw) {
|
|
14
|
+
if (typeof raw !== 'string') return null;
|
|
15
|
+
const lower = raw.trim().toLowerCase();
|
|
16
|
+
return VALID_CHANNELS.has(lower) ? lower : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Conservative quoting for tag names passed to git commands. Tags are
|
|
21
|
+
* user-typed (--pin) or come from the GitHub API. Only allow the semver
|
|
22
|
+
* character class we use to tag BMad releases; anything else throws.
|
|
23
|
+
*/
|
|
24
|
+
function quoteShell(ref) {
|
|
25
|
+
if (typeof ref !== 'string' || !/^[\w.\-+/]+$/.test(ref)) {
|
|
26
|
+
throw new Error(`Unsafe ref name: ${JSON.stringify(ref)}`);
|
|
27
|
+
}
|
|
28
|
+
return `"${ref}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readChannelMarker(markerPath) {
|
|
32
|
+
try {
|
|
33
|
+
if (!(await fs.pathExists(markerPath))) return null;
|
|
34
|
+
const content = await fs.readFile(markerPath, 'utf8');
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function writeChannelMarker(markerPath, data) {
|
|
42
|
+
try {
|
|
43
|
+
await fs.writeFile(markerPath, JSON.stringify({ ...data, writtenAt: new Date().toISOString() }, null, 2));
|
|
44
|
+
} catch {
|
|
45
|
+
// Best-effort: marker is an optimization, not a correctness requirement.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MARKETPLACE_OWNER = 'bmad-code-org';
|
|
50
|
+
const MARKETPLACE_REPO = 'bmad-plugins-marketplace';
|
|
51
|
+
const MARKETPLACE_REF = 'main';
|
|
10
52
|
const FALLBACK_CONFIG_PATH = path.join(__dirname, 'registry-fallback.yaml');
|
|
11
53
|
|
|
12
54
|
/**
|
|
@@ -17,10 +59,25 @@ const FALLBACK_CONFIG_PATH = path.join(__dirname, 'registry-fallback.yaml');
|
|
|
17
59
|
* @class ExternalModuleManager
|
|
18
60
|
*/
|
|
19
61
|
class ExternalModuleManager {
|
|
62
|
+
// moduleCode → { channel, version, ref, sha, repoUrl, resolvedFallback }
|
|
63
|
+
// Populated when cloneExternalModule resolves a channel. Shared across all
|
|
64
|
+
// instances so the manifest writer (which often instantiates a fresh
|
|
65
|
+
// ExternalModuleManager) sees resolutions made during install.
|
|
66
|
+
static _resolutions = new Map();
|
|
67
|
+
|
|
20
68
|
constructor() {
|
|
21
69
|
this._client = new RegistryClient();
|
|
22
70
|
}
|
|
23
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Get the most recent channel resolution for a module (if any).
|
|
74
|
+
* @param {string} moduleCode
|
|
75
|
+
* @returns {Object|null}
|
|
76
|
+
*/
|
|
77
|
+
getResolution(moduleCode) {
|
|
78
|
+
return ExternalModuleManager._resolutions.get(moduleCode) || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
24
81
|
/**
|
|
25
82
|
* Load the official modules registry from GitHub, falling back to the
|
|
26
83
|
* bundled YAML file if the fetch fails.
|
|
@@ -33,8 +90,7 @@ class ExternalModuleManager {
|
|
|
33
90
|
|
|
34
91
|
// Try remote registry first
|
|
35
92
|
try {
|
|
36
|
-
const
|
|
37
|
-
const config = yaml.parse(content);
|
|
93
|
+
const config = await this._client.fetchGitHubYaml(MARKETPLACE_OWNER, MARKETPLACE_REPO, 'registry/official.yaml', MARKETPLACE_REF);
|
|
38
94
|
if (config?.modules?.length) {
|
|
39
95
|
this.cachedModules = config;
|
|
40
96
|
return config;
|
|
@@ -74,6 +130,7 @@ class ExternalModuleManager {
|
|
|
74
130
|
defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
|
|
75
131
|
type: mod.type || 'bmad-org',
|
|
76
132
|
npmPackage: mod.npm_package || mod.npmPackage || null,
|
|
133
|
+
defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable',
|
|
77
134
|
builtIn: mod.built_in === true,
|
|
78
135
|
isExternal: mod.built_in !== true,
|
|
79
136
|
};
|
|
@@ -119,10 +176,15 @@ class ExternalModuleManager {
|
|
|
119
176
|
}
|
|
120
177
|
|
|
121
178
|
/**
|
|
122
|
-
* Clone an external module repository to cache
|
|
179
|
+
* Clone an external module repository to cache, resolving the requested
|
|
180
|
+
* channel (stable / next / pinned) to a concrete git ref.
|
|
181
|
+
*
|
|
123
182
|
* @param {string} moduleCode - Code of the external module
|
|
124
183
|
* @param {Object} options - Clone options
|
|
125
|
-
* @param {boolean} options.silent - Suppress spinner output
|
|
184
|
+
* @param {boolean} [options.silent] - Suppress spinner output
|
|
185
|
+
* @param {Object} [options.channelOptions] - Parsed channel flags. See
|
|
186
|
+
* modules/channel-plan.js. When absent, the module installs on its
|
|
187
|
+
* registry-declared default channel (typically 'stable').
|
|
126
188
|
* @returns {string} Path to the cloned repository
|
|
127
189
|
*/
|
|
128
190
|
async cloneExternalModule(moduleCode, options = {}) {
|
|
@@ -160,38 +222,160 @@ class ExternalModuleManager {
|
|
|
160
222
|
return await prompts.spinner();
|
|
161
223
|
};
|
|
162
224
|
|
|
163
|
-
//
|
|
225
|
+
// ─── Resolve channel plan ─────────────────────────────────────────────
|
|
226
|
+
// Post-install callers (config generation, directory setup, help catalog
|
|
227
|
+
// rebuild) invoke findModuleSource/cloneExternalModule without
|
|
228
|
+
// channelOptions just to locate the module's files. Those calls must not
|
|
229
|
+
// redecide the channel — the install step already chose one, cloned the
|
|
230
|
+
// right ref, and recorded a resolution. If we re-resolve without flags,
|
|
231
|
+
// we'd snap back to stable and overwrite a pinned install.
|
|
232
|
+
const hasExplicitChannelInput =
|
|
233
|
+
options.channelOptions &&
|
|
234
|
+
(options.channelOptions.global ||
|
|
235
|
+
(options.channelOptions.nextSet && options.channelOptions.nextSet.size > 0) ||
|
|
236
|
+
(options.channelOptions.pins && options.channelOptions.pins.size > 0));
|
|
237
|
+
const existingResolution = ExternalModuleManager._resolutions.get(moduleCode);
|
|
238
|
+
const haveUsableCache = await fs.pathExists(moduleCacheDir);
|
|
239
|
+
|
|
240
|
+
if (!hasExplicitChannelInput && existingResolution && haveUsableCache) {
|
|
241
|
+
// This is a look-up only; the module is already installed at its chosen
|
|
242
|
+
// ref. Skip cloning and return the cached path unchanged.
|
|
243
|
+
return moduleCacheDir;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const planEntry = decideChannelForModule({
|
|
247
|
+
code: moduleCode,
|
|
248
|
+
channelOptions: options.channelOptions,
|
|
249
|
+
registryDefault: moduleInfo.defaultChannel,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Same-plan short-circuit: a single install calls cloneExternalModule
|
|
253
|
+
// several times (config collection, directory setup, help-catalog rebuild)
|
|
254
|
+
// with the same channelOptions. The first call resolves + clones; later
|
|
255
|
+
// calls with an identical plan and a valid cache should return immediately
|
|
256
|
+
// instead of re-running resolveChannel() and `git fetch` (slow; can fail
|
|
257
|
+
// on flaky networks even though the tagCache dedupes the GitHub API hit).
|
|
258
|
+
if (existingResolution && haveUsableCache && existingResolution.channel === planEntry.channel) {
|
|
259
|
+
const samePin = planEntry.channel !== 'pinned' || existingResolution.version === planEntry.pin;
|
|
260
|
+
if (samePin) return moduleCacheDir;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let resolved;
|
|
264
|
+
try {
|
|
265
|
+
resolved = await resolveChannel({
|
|
266
|
+
channel: planEntry.channel,
|
|
267
|
+
pin: planEntry.pin,
|
|
268
|
+
repoUrl: moduleInfo.url,
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
// Tag-API failure (rate limit, transient network). If we already have
|
|
272
|
+
// a usable cache at a recorded ref, treat this as "couldn't check for
|
|
273
|
+
// updates" and re-use the cached version silently — that's the right
|
|
274
|
+
// call for an update/quick-update, since the semantics don't change
|
|
275
|
+
// and the user isn't worse off than before they ran this command.
|
|
276
|
+
const cachedMarker = await readChannelMarker(path.join(moduleCacheDir, '.bmad-channel.json'));
|
|
277
|
+
if (cachedMarker?.channel && (await fs.pathExists(moduleCacheDir))) {
|
|
278
|
+
if (!silent) {
|
|
279
|
+
await prompts.log.warn(
|
|
280
|
+
`Could not check for updates to ${moduleInfo.name} (${error.message}); using cached ${cachedMarker.version || cachedMarker.channel}.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
ExternalModuleManager._resolutions.set(moduleCode, {
|
|
284
|
+
channel: cachedMarker.channel,
|
|
285
|
+
version: cachedMarker.version || 'main',
|
|
286
|
+
ref: cachedMarker.version && cachedMarker.version !== 'main' ? cachedMarker.version : null,
|
|
287
|
+
sha: cachedMarker.sha,
|
|
288
|
+
repoUrl: moduleInfo.url,
|
|
289
|
+
resolvedFallback: false,
|
|
290
|
+
planSource: 'cached',
|
|
291
|
+
});
|
|
292
|
+
return moduleCacheDir;
|
|
293
|
+
}
|
|
294
|
+
// No cache to fall back on — this is effectively a fresh install with
|
|
295
|
+
// no offline safety net. Surface a clear error with actionable guidance.
|
|
296
|
+
const isRateLimited = /rate limit/i.test(error.message);
|
|
297
|
+
const hint = isRateLimited
|
|
298
|
+
? process.env.GITHUB_TOKEN
|
|
299
|
+
? 'Your GITHUB_TOKEN may have expired or been rate-limited on its own budget. Try a different token or wait for the reset.'
|
|
300
|
+
: 'Set a GITHUB_TOKEN env var (any personal access token with public-repo read) to raise the 60-req/hour anonymous limit.'
|
|
301
|
+
: `Check your network connection, or rerun with \`--next=${moduleCode}\` / \`--pin ${moduleCode}=<tag>\` to skip the tag lookup.`;
|
|
302
|
+
throw new Error(`Could not resolve stable tag for '${moduleCode}' (${error.message}). ${hint}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (resolved.resolvedFallback && !silent) {
|
|
306
|
+
if (resolved.reason === 'no-stable-tags') {
|
|
307
|
+
await prompts.log.warn(`No stable releases found for ${moduleInfo.name}; installing from main.`);
|
|
308
|
+
} else if (resolved.reason === 'not-a-github-url') {
|
|
309
|
+
await prompts.log.warn(`Cannot determine stable tags for ${moduleInfo.name} (non-GitHub URL); installing from main.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Validate pin before we burn time cloning. Best-effort: skip on non-GitHub URLs.
|
|
314
|
+
if (planEntry.channel === 'pinned') {
|
|
315
|
+
const parsed = parseGitHubRepo(moduleInfo.url);
|
|
316
|
+
if (parsed) {
|
|
317
|
+
try {
|
|
318
|
+
const exists = await tagExists(parsed.owner, parsed.repo, planEntry.pin);
|
|
319
|
+
if (!exists) {
|
|
320
|
+
throw new Error(`Tag '${planEntry.pin}' not found in ${parsed.owner}/${parsed.repo}.`);
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error.message?.includes('not found')) throw error;
|
|
324
|
+
// Network hiccup on tag verification — let the clone attempt fail clearly.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Clone or update cache by resolved channel ────────────────────────
|
|
330
|
+
const markerPath = path.join(moduleCacheDir, '.bmad-channel.json');
|
|
331
|
+
const currentMarker = await readChannelMarker(markerPath);
|
|
332
|
+
const needsChannelReset = currentMarker && currentMarker.channel !== resolved.channel;
|
|
333
|
+
|
|
164
334
|
let needsDependencyInstall = false;
|
|
165
335
|
let wasNewClone = false;
|
|
166
336
|
|
|
167
|
-
|
|
337
|
+
if (needsChannelReset && (await fs.pathExists(moduleCacheDir))) {
|
|
338
|
+
// Channel changed (e.g. user switched stable→next). Blow away and re-clone
|
|
339
|
+
// to avoid tangling shallow clones of different refs.
|
|
340
|
+
await fs.remove(moduleCacheDir);
|
|
341
|
+
}
|
|
342
|
+
|
|
168
343
|
if (await fs.pathExists(moduleCacheDir)) {
|
|
169
|
-
//
|
|
344
|
+
// Cache exists on the right channel. Refresh the ref.
|
|
170
345
|
const fetchSpinner = await createSpinner();
|
|
171
346
|
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
|
|
172
347
|
try {
|
|
173
|
-
const
|
|
174
|
-
// Fetch and reset to remote - works better with shallow clones than pull
|
|
175
|
-
execSync('git fetch origin --depth 1', {
|
|
176
|
-
cwd: moduleCacheDir,
|
|
177
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
178
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
179
|
-
});
|
|
180
|
-
execSync('git reset --hard origin/HEAD', {
|
|
181
|
-
cwd: moduleCacheDir,
|
|
182
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
184
|
-
});
|
|
185
|
-
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
|
348
|
+
const currentSha = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
|
186
349
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
350
|
+
if (resolved.channel === 'next') {
|
|
351
|
+
execSync('git fetch origin --depth 1', {
|
|
352
|
+
cwd: moduleCacheDir,
|
|
353
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
354
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
355
|
+
});
|
|
356
|
+
execSync('git reset --hard origin/HEAD', {
|
|
357
|
+
cwd: moduleCacheDir,
|
|
358
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
359
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
360
|
+
});
|
|
361
|
+
} else {
|
|
362
|
+
// stable or pinned — fetch the specific tag and check it out.
|
|
363
|
+
execSync(`git fetch --depth 1 origin tag ${quoteShell(resolved.ref)} --no-tags`, {
|
|
364
|
+
cwd: moduleCacheDir,
|
|
365
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
366
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
367
|
+
});
|
|
368
|
+
execSync(`git checkout --quiet FETCH_HEAD`, {
|
|
369
|
+
cwd: moduleCacheDir,
|
|
370
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
371
|
+
});
|
|
191
372
|
}
|
|
373
|
+
|
|
374
|
+
const newSha = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
|
375
|
+
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
|
|
376
|
+
if (currentSha !== newSha) needsDependencyInstall = true;
|
|
192
377
|
} catch {
|
|
193
378
|
fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`);
|
|
194
|
-
// If update fails, remove and re-clone
|
|
195
379
|
await fs.remove(moduleCacheDir);
|
|
196
380
|
wasNewClone = true;
|
|
197
381
|
}
|
|
@@ -199,22 +383,41 @@ class ExternalModuleManager {
|
|
|
199
383
|
wasNewClone = true;
|
|
200
384
|
}
|
|
201
385
|
|
|
202
|
-
// Clone if not exists or was removed
|
|
203
386
|
if (wasNewClone) {
|
|
204
387
|
const fetchSpinner = await createSpinner();
|
|
205
388
|
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
|
|
206
389
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
390
|
+
if (resolved.channel === 'next') {
|
|
391
|
+
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
|
|
392
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
393
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
execSync(`git clone --depth 1 --branch ${quoteShell(resolved.ref)} "${moduleInfo.url}" "${moduleCacheDir}"`, {
|
|
397
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
398
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
399
|
+
});
|
|
400
|
+
}
|
|
211
401
|
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
|
|
212
402
|
} catch (error) {
|
|
213
403
|
fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`);
|
|
214
|
-
throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
|
|
404
|
+
throw new Error(`Failed to clone external module '${moduleCode}' at ${resolved.version}: ${error.message}`);
|
|
215
405
|
}
|
|
216
406
|
}
|
|
217
407
|
|
|
408
|
+
// Record resolution (channel + tag + SHA) for the manifest writer to pick up.
|
|
409
|
+
const sha = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
|
|
410
|
+
ExternalModuleManager._resolutions.set(moduleCode, {
|
|
411
|
+
channel: resolved.channel,
|
|
412
|
+
version: resolved.version,
|
|
413
|
+
ref: resolved.ref,
|
|
414
|
+
sha,
|
|
415
|
+
repoUrl: moduleInfo.url,
|
|
416
|
+
resolvedFallback: !!resolved.resolvedFallback,
|
|
417
|
+
planSource: planEntry.source,
|
|
418
|
+
});
|
|
419
|
+
await writeChannelMarker(markerPath, { channel: resolved.channel, version: resolved.version, sha });
|
|
420
|
+
|
|
218
421
|
// Install dependencies if package.json exists
|
|
219
422
|
const packageJsonPath = path.join(moduleCacheDir, 'package.json');
|
|
220
423
|
const nodeModulesPath = path.join(moduleCacheDir, 'node_modules');
|