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
package/tools/installer/ui.js
CHANGED
|
@@ -1,50 +1,107 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
const os = require('node:os');
|
|
3
|
+
const semver = require('semver');
|
|
3
4
|
const fs = require('./fs-native');
|
|
4
5
|
const { CLIUtils } = require('./cli-utils');
|
|
5
6
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
|
6
|
-
const {
|
|
7
|
+
const { resolveModuleVersion } = require('./modules/version-resolver');
|
|
8
|
+
const { Manifest } = require('./core/manifest');
|
|
9
|
+
const {
|
|
10
|
+
parseChannelOptions,
|
|
11
|
+
buildPlan,
|
|
12
|
+
decideChannelForModule,
|
|
13
|
+
orphanPinWarnings,
|
|
14
|
+
bundledTargetWarnings,
|
|
15
|
+
} = require('./modules/channel-plan');
|
|
16
|
+
const channelResolver = require('./modules/channel-resolver');
|
|
7
17
|
const prompts = require('./prompts');
|
|
8
18
|
|
|
19
|
+
const manifest = new Manifest();
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @
|
|
22
|
+
* Format a resolved version for display in installer labels.
|
|
23
|
+
* Semver-like values are normalized to a single leading "v".
|
|
24
|
+
* @param {string|null|undefined} version
|
|
25
|
+
* @returns {string}
|
|
13
26
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
function formatDisplayVersion(version) {
|
|
28
|
+
const trimmed = typeof version === 'string' ? version.trim() : '';
|
|
29
|
+
if (!trimmed) return '';
|
|
30
|
+
|
|
31
|
+
const normalized = semver.valid(semver.coerce(trimmed));
|
|
32
|
+
if (normalized) {
|
|
33
|
+
return `v${normalized}`;
|
|
21
34
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the display label for a module, showing an upgrade arrow when an
|
|
41
|
+
* installed semver differs from the latest resolvable semver.
|
|
42
|
+
* @param {string} name
|
|
43
|
+
* @param {string} latestVersion
|
|
44
|
+
* @param {string} installedVersion
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function buildModuleLabel(name, latestVersion, installedVersion = '') {
|
|
48
|
+
const latestDisplay = formatDisplayVersion(latestVersion);
|
|
49
|
+
if (!latestDisplay) return name;
|
|
50
|
+
|
|
51
|
+
const installedDisplay = formatDisplayVersion(installedVersion);
|
|
52
|
+
const latestSemver = semver.valid(semver.coerce(latestVersion || ''));
|
|
53
|
+
const installedSemver = semver.valid(semver.coerce(installedVersion || ''));
|
|
54
|
+
|
|
55
|
+
if (installedDisplay && latestSemver && installedSemver && semver.neq(installedSemver, latestSemver)) {
|
|
56
|
+
return `${name} (${installedDisplay} → ${latestDisplay})`;
|
|
29
57
|
}
|
|
30
|
-
|
|
58
|
+
|
|
59
|
+
return `${name} (${latestDisplay})`;
|
|
31
60
|
}
|
|
32
61
|
|
|
33
62
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
63
|
+
* Resolve the version to show for a module picker entry. External modules use
|
|
64
|
+
* the same channel/tag resolver as installs; bundled modules fall back to local
|
|
65
|
+
* source metadata.
|
|
66
|
+
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
|
67
|
+
* @param {Object} options
|
|
68
|
+
* @param {string|null} [options.repoUrl] - Module repository URL for tag resolution
|
|
69
|
+
* @param {string|null} [options.registryDefault] - Registry default channel
|
|
70
|
+
* @param {Object|null} [options.channelOptions] - Parsed installer channel options
|
|
71
|
+
* @returns {Promise<{version: string, lookupAttempted: boolean, lookupSucceeded: boolean}>}
|
|
38
72
|
*/
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault = null, channelOptions = null } = {}) {
|
|
74
|
+
if (repoUrl) {
|
|
75
|
+
const plan = decideChannelForModule({
|
|
76
|
+
code: moduleCode,
|
|
77
|
+
channelOptions,
|
|
78
|
+
registryDefault,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const resolved = await channelResolver.resolveChannel({
|
|
83
|
+
channel: plan.channel,
|
|
84
|
+
pin: plan.pin,
|
|
85
|
+
repoUrl,
|
|
86
|
+
});
|
|
87
|
+
if (resolved?.version) {
|
|
88
|
+
return {
|
|
89
|
+
version: resolved.version,
|
|
90
|
+
lookupAttempted: plan.channel === 'stable',
|
|
91
|
+
lookupSucceeded: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Fall back to local metadata when tag resolution is unavailable.
|
|
96
|
+
}
|
|
46
97
|
}
|
|
47
|
-
|
|
98
|
+
|
|
99
|
+
const versionInfo = await resolveModuleVersion(moduleCode);
|
|
100
|
+
return {
|
|
101
|
+
version: versionInfo.version || '',
|
|
102
|
+
lookupAttempted: !!repoUrl,
|
|
103
|
+
lookupSucceeded: false,
|
|
104
|
+
};
|
|
48
105
|
}
|
|
49
106
|
|
|
50
107
|
/**
|
|
@@ -64,6 +121,13 @@ class UI {
|
|
|
64
121
|
const messageLoader = new MessageLoader();
|
|
65
122
|
await messageLoader.displayStartMessage();
|
|
66
123
|
|
|
124
|
+
// Parse channel flags (--channel/--all-*/--next=/--pin) once. Warnings
|
|
125
|
+
// are surfaced immediately so the user sees them before any git ops run.
|
|
126
|
+
const channelOptions = parseChannelOptions(options);
|
|
127
|
+
for (const warning of channelOptions.warnings) {
|
|
128
|
+
await prompts.log.warn(warning);
|
|
129
|
+
}
|
|
130
|
+
|
|
67
131
|
// Get directory from options or prompt
|
|
68
132
|
let confirmedDirectory;
|
|
69
133
|
if (options.directory) {
|
|
@@ -145,7 +209,7 @@ class UI {
|
|
|
145
209
|
// Return early with modify configuration
|
|
146
210
|
if (actionType === 'update') {
|
|
147
211
|
// Get existing installation info
|
|
148
|
-
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
|
212
|
+
const { installedModuleIds, installedModuleVersions } = await this.getExistingInstallation(confirmedDirectory);
|
|
149
213
|
|
|
150
214
|
await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);
|
|
151
215
|
|
|
@@ -167,7 +231,7 @@ class UI {
|
|
|
167
231
|
`Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
|
|
168
232
|
);
|
|
169
233
|
} else {
|
|
170
|
-
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
234
|
+
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
|
|
171
235
|
}
|
|
172
236
|
|
|
173
237
|
// Resolve custom sources from --custom-source flag
|
|
@@ -183,10 +247,38 @@ class UI {
|
|
|
183
247
|
selectedModules.unshift('core');
|
|
184
248
|
}
|
|
185
249
|
|
|
250
|
+
// For existing installs, resolve per-module update decisions BEFORE
|
|
251
|
+
// we clone anything. Reads the existing manifest's recorded channel
|
|
252
|
+
// per module and prompts the user on available upgrades (patch/minor
|
|
253
|
+
// default Y, major default N). Legacy entries with no channel are
|
|
254
|
+
// migrated here too. Mutates channelOptions.pins to lock rejections.
|
|
255
|
+
await this._resolveUpdateChannels({
|
|
256
|
+
bmadDir,
|
|
257
|
+
selectedModules,
|
|
258
|
+
channelOptions,
|
|
259
|
+
yes: options.yes || false,
|
|
260
|
+
});
|
|
261
|
+
|
|
186
262
|
// Get tool selection
|
|
187
263
|
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
|
188
264
|
|
|
189
|
-
const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules,
|
|
265
|
+
const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
|
|
266
|
+
...options,
|
|
267
|
+
channelOptions,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Warn about --pin/--next flags that refer to modules the user didn't
|
|
271
|
+
// select, or that target bundled modules (core/bmm) where channel
|
|
272
|
+
// flags don't apply.
|
|
273
|
+
{
|
|
274
|
+
const bundledCodes = await this._bundledModuleCodes();
|
|
275
|
+
for (const warning of [
|
|
276
|
+
...orphanPinWarnings(channelOptions, selectedModules),
|
|
277
|
+
...bundledTargetWarnings(channelOptions, bundledCodes),
|
|
278
|
+
]) {
|
|
279
|
+
await prompts.log.warn(warning);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
190
282
|
|
|
191
283
|
return {
|
|
192
284
|
actionType: 'update',
|
|
@@ -197,12 +289,13 @@ class UI {
|
|
|
197
289
|
coreConfig: moduleConfigs.core || {},
|
|
198
290
|
moduleConfigs: moduleConfigs,
|
|
199
291
|
skipPrompts: options.yes || false,
|
|
292
|
+
channelOptions,
|
|
200
293
|
};
|
|
201
294
|
}
|
|
202
295
|
}
|
|
203
296
|
|
|
204
297
|
// This section is only for new installations (update returns early above)
|
|
205
|
-
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
|
298
|
+
const { installedModuleIds, installedModuleVersions } = await this.getExistingInstallation(confirmedDirectory);
|
|
206
299
|
|
|
207
300
|
// Unified module selection - all modules in one grouped multiselect
|
|
208
301
|
let selectedModules;
|
|
@@ -221,7 +314,7 @@ class UI {
|
|
|
221
314
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
|
222
315
|
await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
|
|
223
316
|
} else {
|
|
224
|
-
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
317
|
+
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
|
|
225
318
|
}
|
|
226
319
|
|
|
227
320
|
// Resolve custom sources from --custom-source flag
|
|
@@ -236,8 +329,31 @@ class UI {
|
|
|
236
329
|
if (!selectedModules.includes('core')) {
|
|
237
330
|
selectedModules.unshift('core');
|
|
238
331
|
}
|
|
332
|
+
|
|
333
|
+
// Interactive channel gate: "Ready to install (all stable)? [Y/n]"
|
|
334
|
+
// Only shown for fresh installs with no channel flags and an external module
|
|
335
|
+
// selected. Non-interactive installs skip this and fall through to the
|
|
336
|
+
// registry default (stable) or whatever flags were supplied.
|
|
337
|
+
await this._interactiveChannelGate({ options, channelOptions, selectedModules });
|
|
338
|
+
|
|
239
339
|
let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
|
240
|
-
const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules,
|
|
340
|
+
const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
|
|
341
|
+
...options,
|
|
342
|
+
channelOptions,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Warn about --pin/--next flags that refer to modules the user didn't
|
|
346
|
+
// select, or that target bundled modules (core/bmm) where channel
|
|
347
|
+
// flags don't apply.
|
|
348
|
+
{
|
|
349
|
+
const bundledCodes = await this._bundledModuleCodes();
|
|
350
|
+
for (const warning of [
|
|
351
|
+
...orphanPinWarnings(channelOptions, selectedModules),
|
|
352
|
+
...bundledTargetWarnings(channelOptions, bundledCodes),
|
|
353
|
+
]) {
|
|
354
|
+
await prompts.log.warn(warning);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
241
357
|
|
|
242
358
|
return {
|
|
243
359
|
actionType: 'install',
|
|
@@ -248,6 +364,7 @@ class UI {
|
|
|
248
364
|
coreConfig: moduleConfigs.core || {},
|
|
249
365
|
moduleConfigs: moduleConfigs,
|
|
250
366
|
skipPrompts: options.yes || false,
|
|
367
|
+
channelOptions,
|
|
251
368
|
};
|
|
252
369
|
}
|
|
253
370
|
|
|
@@ -496,7 +613,7 @@ class UI {
|
|
|
496
613
|
/**
|
|
497
614
|
* Get existing installation info and installed modules
|
|
498
615
|
* @param {string} directory - Installation directory
|
|
499
|
-
* @returns {Object} Object with existingInstall, installedModuleIds, and bmadDir
|
|
616
|
+
* @returns {Object} Object with existingInstall, installedModuleIds, installedModuleVersions, and bmadDir
|
|
500
617
|
*/
|
|
501
618
|
async getExistingInstallation(directory) {
|
|
502
619
|
const { ExistingInstall } = require('./core/existing-install');
|
|
@@ -505,8 +622,26 @@ class UI {
|
|
|
505
622
|
const { bmadDir } = await installer.findBmadDir(directory);
|
|
506
623
|
const existingInstall = await ExistingInstall.detect(bmadDir);
|
|
507
624
|
const installedModuleIds = new Set(existingInstall.moduleIds);
|
|
625
|
+
const installedModuleVersions = new Map();
|
|
626
|
+
const manifestModules = await manifest.getAllModuleVersions(bmadDir);
|
|
508
627
|
|
|
509
|
-
|
|
628
|
+
for (const module of manifestModules) {
|
|
629
|
+
if (module?.name && module.version) {
|
|
630
|
+
installedModuleVersions.set(module.name, module.version);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const module of existingInstall.modules) {
|
|
635
|
+
if (module?.id && module.version && module.version !== 'unknown' && !installedModuleVersions.has(module.id)) {
|
|
636
|
+
installedModuleVersions.set(module.id, module.version);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (existingInstall.hasCore && existingInstall.version && !installedModuleVersions.has('core')) {
|
|
641
|
+
installedModuleVersions.set('core', existingInstall.version);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return { existingInstall, installedModuleIds, installedModuleVersions, bmadDir };
|
|
510
645
|
}
|
|
511
646
|
|
|
512
647
|
/**
|
|
@@ -519,7 +654,7 @@ class UI {
|
|
|
519
654
|
*/
|
|
520
655
|
async collectModuleConfigs(directory, modules, options = {}) {
|
|
521
656
|
const { OfficialModules } = require('./modules/official-modules');
|
|
522
|
-
const configCollector = new OfficialModules();
|
|
657
|
+
const configCollector = new OfficialModules({ channelOptions: options.channelOptions });
|
|
523
658
|
|
|
524
659
|
// Seed core config from CLI options if provided
|
|
525
660
|
if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) {
|
|
@@ -587,11 +722,13 @@ class UI {
|
|
|
587
722
|
/**
|
|
588
723
|
* Select all modules across three tiers: official, community, and custom URL.
|
|
589
724
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
725
|
+
* @param {Map<string, string>} installedModuleVersions - Installed module versions from the local manifest
|
|
726
|
+
* @param {Object|null} channelOptions - Parsed installer channel options
|
|
590
727
|
* @returns {Array} Selected module codes (excluding core)
|
|
591
728
|
*/
|
|
592
|
-
async selectAllModules(installedModuleIds = new Set()) {
|
|
729
|
+
async selectAllModules(installedModuleIds = new Set(), installedModuleVersions = new Map(), channelOptions = null) {
|
|
593
730
|
// Phase 1: Official modules
|
|
594
|
-
const officialSelected = await this._selectOfficialModules(installedModuleIds);
|
|
731
|
+
const officialSelected = await this._selectOfficialModules(installedModuleIds, installedModuleVersions, channelOptions);
|
|
595
732
|
|
|
596
733
|
// Determine which installed modules are NOT official (community or custom).
|
|
597
734
|
// These must be preserved even if the user declines to browse community/custom.
|
|
@@ -627,9 +764,11 @@ class UI {
|
|
|
627
764
|
* Select official modules using autocompleteMultiselect.
|
|
628
765
|
* Extracted from the original selectAllModules - unchanged behavior.
|
|
629
766
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
767
|
+
* @param {Map<string, string>} installedModuleVersions - Installed module versions from the local manifest
|
|
768
|
+
* @param {Object|null} channelOptions - Parsed installer channel options
|
|
630
769
|
* @returns {Array} Selected official module codes
|
|
631
770
|
*/
|
|
632
|
-
async _selectOfficialModules(installedModuleIds = new Set()) {
|
|
771
|
+
async _selectOfficialModules(installedModuleIds = new Set(), installedModuleVersions = new Map(), channelOptions = null) {
|
|
633
772
|
// Built-in modules (core, bmm) come from local source, not the registry
|
|
634
773
|
const { OfficialModules } = require('./modules/official-modules');
|
|
635
774
|
const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
|
|
@@ -642,15 +781,18 @@ class UI {
|
|
|
642
781
|
const initialValues = [];
|
|
643
782
|
const lockedValues = ['core'];
|
|
644
783
|
|
|
645
|
-
const buildModuleEntry = async (code, name, description, isDefault) => {
|
|
784
|
+
const buildModuleEntry = async (code, name, description, isDefault, repoUrl = null, registryDefault = null) => {
|
|
646
785
|
const isInstalled = installedModuleIds.has(code);
|
|
647
|
-
const
|
|
648
|
-
const
|
|
786
|
+
const installedVersion = installedModuleVersions.get(code) || '';
|
|
787
|
+
const versionState = await getModuleVersion(code, { repoUrl, registryDefault, channelOptions });
|
|
788
|
+
const label = buildModuleLabel(name, versionState.version, installedVersion);
|
|
649
789
|
return {
|
|
650
790
|
label,
|
|
651
791
|
value: code,
|
|
652
792
|
hint: description,
|
|
653
793
|
selected: isInstalled || isDefault,
|
|
794
|
+
lookupAttempted: versionState.lookupAttempted,
|
|
795
|
+
lookupSucceeded: versionState.lookupSucceeded,
|
|
654
796
|
};
|
|
655
797
|
};
|
|
656
798
|
|
|
@@ -667,12 +809,38 @@ class UI {
|
|
|
667
809
|
}
|
|
668
810
|
|
|
669
811
|
// Add external registry modules (skip built-in duplicates)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
812
|
+
const externalRegistryModules = registryModules.filter((mod) => !mod.builtIn && !builtInCodes.has(mod.code));
|
|
813
|
+
let externalRegistryEntries = [];
|
|
814
|
+
if (externalRegistryModules.length > 0) {
|
|
815
|
+
const spinner = await prompts.spinner();
|
|
816
|
+
spinner.start('Checking latest module versions...');
|
|
817
|
+
|
|
818
|
+
externalRegistryEntries = await Promise.all(
|
|
819
|
+
externalRegistryModules.map(async (mod) => ({
|
|
820
|
+
code: mod.code,
|
|
821
|
+
entry: await buildModuleEntry(
|
|
822
|
+
mod.code,
|
|
823
|
+
mod.name,
|
|
824
|
+
mod.description,
|
|
825
|
+
mod.defaultSelected,
|
|
826
|
+
mod.url || null,
|
|
827
|
+
mod.defaultChannel || null,
|
|
828
|
+
),
|
|
829
|
+
})),
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
spinner.stop('Checked latest module versions.');
|
|
833
|
+
|
|
834
|
+
const attemptedLookups = externalRegistryEntries.filter(({ entry }) => entry.lookupAttempted).length;
|
|
835
|
+
const successfulLookups = externalRegistryEntries.filter(({ entry }) => entry.lookupSucceeded).length;
|
|
836
|
+
if (attemptedLookups > 0 && successfulLookups === 0) {
|
|
837
|
+
await prompts.log.warn('Could not check latest module versions; showing cached/local versions.');
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
for (const { code, entry } of externalRegistryEntries) {
|
|
673
841
|
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
|
674
842
|
if (entry.selected) {
|
|
675
|
-
initialValues.push(
|
|
843
|
+
initialValues.push(code);
|
|
676
844
|
}
|
|
677
845
|
}
|
|
678
846
|
|
|
@@ -1594,6 +1762,349 @@ class UI {
|
|
|
1594
1762
|
});
|
|
1595
1763
|
await prompts.log.message('Selected tools:\n' + toolLines.join('\n'));
|
|
1596
1764
|
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Return the set of module codes the registry marks as built-in (core, bmm).
|
|
1768
|
+
* These ship with the installer binary and have no per-module channel.
|
|
1769
|
+
*/
|
|
1770
|
+
async _bundledModuleCodes() {
|
|
1771
|
+
const externalManager = new ExternalModuleManager();
|
|
1772
|
+
try {
|
|
1773
|
+
const modules = await externalManager.listAvailable();
|
|
1774
|
+
return modules.filter((m) => m.builtIn).map((m) => m.code);
|
|
1775
|
+
} catch {
|
|
1776
|
+
// Registry unreachable — fall back to the known bundled codes.
|
|
1777
|
+
return ['core', 'bmm'];
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Fast-path channel gate: confirm "all stable" or open the per-module picker.
|
|
1783
|
+
*
|
|
1784
|
+
* Skipped when:
|
|
1785
|
+
* - running non-interactively (--yes)
|
|
1786
|
+
* - the user already passed channel flags (--channel / --pin / --next)
|
|
1787
|
+
* - no externals/community modules are selected
|
|
1788
|
+
*
|
|
1789
|
+
* Mutates channelOptions.pins and channelOptions.nextSet to reflect picker choices.
|
|
1790
|
+
*/
|
|
1791
|
+
async _interactiveChannelGate({ options, channelOptions, selectedModules }) {
|
|
1792
|
+
if (options.yes) return;
|
|
1793
|
+
// If the user already declared their channel intent via flags, trust them
|
|
1794
|
+
// and skip the gate.
|
|
1795
|
+
const haveFlagIntent = channelOptions.global || channelOptions.nextSet.size > 0 || channelOptions.pins.size > 0;
|
|
1796
|
+
if (haveFlagIntent) return;
|
|
1797
|
+
|
|
1798
|
+
// Figure out which selected modules actually get a channel (externals +
|
|
1799
|
+
// community modules). Bundled core/bmm and custom modules skip the picker.
|
|
1800
|
+
const externalManager = new ExternalModuleManager();
|
|
1801
|
+
const externals = await externalManager.listAvailable();
|
|
1802
|
+
const externalByCode = new Map(externals.map((m) => [m.code, m]));
|
|
1803
|
+
|
|
1804
|
+
const { CommunityModuleManager } = require('./modules/community-manager');
|
|
1805
|
+
const communityMgr = new CommunityModuleManager();
|
|
1806
|
+
const community = await communityMgr.listAll();
|
|
1807
|
+
const communityByCode = new Map(community.map((m) => [m.code, m]));
|
|
1808
|
+
|
|
1809
|
+
const channelSelectable = selectedModules.filter((code) => {
|
|
1810
|
+
const info = externalByCode.get(code) || communityByCode.get(code);
|
|
1811
|
+
return info && !info.builtIn;
|
|
1812
|
+
});
|
|
1813
|
+
if (channelSelectable.length === 0) return;
|
|
1814
|
+
|
|
1815
|
+
const fastPath = await prompts.confirm({
|
|
1816
|
+
message: `Ready to install (all stable)? Pick "n" to customize channels or pin versions.`,
|
|
1817
|
+
default: true,
|
|
1818
|
+
});
|
|
1819
|
+
if (fastPath) return; // stable for all, registry default applies
|
|
1820
|
+
|
|
1821
|
+
// Customize path: per-module picker.
|
|
1822
|
+
const { fetchStableTags, parseGitHubRepo } = require('./modules/channel-resolver');
|
|
1823
|
+
|
|
1824
|
+
for (const code of channelSelectable) {
|
|
1825
|
+
const info = externalByCode.get(code) || communityByCode.get(code);
|
|
1826
|
+
const repoUrl = info.url;
|
|
1827
|
+
|
|
1828
|
+
// Try to pre-resolve the top stable tag so we can surface it in the picker.
|
|
1829
|
+
let stableLabel = 'stable (released version)';
|
|
1830
|
+
try {
|
|
1831
|
+
const parsed = repoUrl ? parseGitHubRepo(repoUrl) : null;
|
|
1832
|
+
if (parsed) {
|
|
1833
|
+
const tags = await fetchStableTags(parsed.owner, parsed.repo);
|
|
1834
|
+
if (tags.length > 0) {
|
|
1835
|
+
stableLabel = `stable ${tags[0].tag} (released version)`;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
} catch {
|
|
1839
|
+
// fall through with the generic label
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const choice = await prompts.select({
|
|
1843
|
+
message: `${code}: choose a channel`,
|
|
1844
|
+
choices: [
|
|
1845
|
+
{ name: stableLabel, value: 'stable' },
|
|
1846
|
+
{ name: 'next (main HEAD \u2014 current development)', value: 'next' },
|
|
1847
|
+
{ name: 'pin (specific version)', value: 'pin' },
|
|
1848
|
+
],
|
|
1849
|
+
default: 'stable',
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
if (choice === 'next') {
|
|
1853
|
+
channelOptions.nextSet.add(code);
|
|
1854
|
+
} else if (choice === 'pin') {
|
|
1855
|
+
const pinValue = await prompts.text({
|
|
1856
|
+
message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
|
|
1857
|
+
validate: (value) => {
|
|
1858
|
+
if (!value || !/^[\w.\-+/]+$/.test(String(value).trim())) {
|
|
1859
|
+
return 'Must be a non-empty tag name (letters, digits, dots, hyphens).';
|
|
1860
|
+
}
|
|
1861
|
+
},
|
|
1862
|
+
});
|
|
1863
|
+
channelOptions.pins.set(code, String(pinValue).trim());
|
|
1864
|
+
}
|
|
1865
|
+
// 'stable' is the default; nothing to record.
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Resolve channel decisions for an update over an existing install.
|
|
1871
|
+
*
|
|
1872
|
+
* For each selected external/community module:
|
|
1873
|
+
* - Read the recorded channel from the existing manifest.
|
|
1874
|
+
* - On `stable`: query tags; if a newer stable exists, classify the diff
|
|
1875
|
+
* and prompt. Patch/minor default Y; major defaults N. `--yes` accepts
|
|
1876
|
+
* defaults (patches/minors) but NOT majors — a major under --yes stays
|
|
1877
|
+
* frozen unless the user also passes `--pin CODE=NEW_TAG`.
|
|
1878
|
+
* - On `next`: no prompt (pull HEAD).
|
|
1879
|
+
* - On `pinned`: no prompt (stays pinned).
|
|
1880
|
+
* - No channel recorded and `version: null`: one-time migration prompt
|
|
1881
|
+
* ("Switch to stable / Keep on next").
|
|
1882
|
+
*
|
|
1883
|
+
* Decisions that freeze the current version are applied by adding a pin to
|
|
1884
|
+
* `channelOptions.pins` so downstream clone logic honors them.
|
|
1885
|
+
*/
|
|
1886
|
+
async _resolveUpdateChannels({ bmadDir, selectedModules, channelOptions, yes }) {
|
|
1887
|
+
const { Manifest } = require('./core/manifest');
|
|
1888
|
+
const manifestObj = new Manifest();
|
|
1889
|
+
const manifest = await manifestObj.read(bmadDir);
|
|
1890
|
+
const existingByName = new Map();
|
|
1891
|
+
for (const m of manifest?.modulesDetailed || []) {
|
|
1892
|
+
if (m?.name) existingByName.set(m.name, m);
|
|
1893
|
+
}
|
|
1894
|
+
if (existingByName.size === 0) return;
|
|
1895
|
+
|
|
1896
|
+
const externalManager = new ExternalModuleManager();
|
|
1897
|
+
const externals = await externalManager.listAvailable();
|
|
1898
|
+
const externalByCode = new Map(externals.map((m) => [m.code, m]));
|
|
1899
|
+
|
|
1900
|
+
const { CommunityModuleManager } = require('./modules/community-manager');
|
|
1901
|
+
const communityMgr = new CommunityModuleManager();
|
|
1902
|
+
const community = await communityMgr.listAll();
|
|
1903
|
+
const communityByCode = new Map(community.map((m) => [m.code, m]));
|
|
1904
|
+
|
|
1905
|
+
const { fetchStableTags, classifyUpgrade, releaseNotesUrl } = require('./modules/channel-resolver');
|
|
1906
|
+
const { parseGitHubRepo } = require('./modules/channel-resolver');
|
|
1907
|
+
|
|
1908
|
+
// Interactive-only: offer a one-time gate to review / switch channels for
|
|
1909
|
+
// selected modules that are already installed. Default N so normal Modify
|
|
1910
|
+
// flows (add/remove modules) aren't interrupted.
|
|
1911
|
+
let reviewChannels = false;
|
|
1912
|
+
if (!yes) {
|
|
1913
|
+
const existingWithChannel = selectedModules.filter((code) => {
|
|
1914
|
+
const prev = existingByName.get(code);
|
|
1915
|
+
if (!prev) return false;
|
|
1916
|
+
const info = externalByCode.get(code) || communityByCode.get(code);
|
|
1917
|
+
return info && !info.builtIn;
|
|
1918
|
+
});
|
|
1919
|
+
if (existingWithChannel.length > 0) {
|
|
1920
|
+
reviewChannels = await prompts.confirm({
|
|
1921
|
+
message: 'Review channel assignments (stable / next / pin) for your existing modules?',
|
|
1922
|
+
default: false,
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
for (const code of selectedModules) {
|
|
1928
|
+
const prev = existingByName.get(code);
|
|
1929
|
+
if (!prev) continue;
|
|
1930
|
+
|
|
1931
|
+
const info = externalByCode.get(code) || communityByCode.get(code);
|
|
1932
|
+
if (!info) continue;
|
|
1933
|
+
// Bundled modules (core/bmm) ship with the installer binary itself —
|
|
1934
|
+
// their version is stapled to the CLI version, not a git tag. Skip
|
|
1935
|
+
// tag-API lookups for them; the "upgrade" mechanism is `npx bmad@X install`.
|
|
1936
|
+
if (info.builtIn) continue;
|
|
1937
|
+
|
|
1938
|
+
const repoUrl = info.url;
|
|
1939
|
+
const parsed = repoUrl ? parseGitHubRepo(repoUrl) : null;
|
|
1940
|
+
|
|
1941
|
+
// Legacy migration: manifest carries no channel and a null/empty
|
|
1942
|
+
// version. Offer the one-time pick between stable and next.
|
|
1943
|
+
const recordedChannel = prev.channel || null;
|
|
1944
|
+
const needsMigration = !recordedChannel && (prev.version == null || prev.version === '');
|
|
1945
|
+
if (needsMigration) {
|
|
1946
|
+
if (yes) {
|
|
1947
|
+
// Conservative headless default: stable.
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
const chosen = await prompts.select({
|
|
1951
|
+
message: `${code}: your existing install tracks the main branch. Switch to stable releases (recommended for production), or keep on main?`,
|
|
1952
|
+
choices: [
|
|
1953
|
+
{ name: 'Switch to stable', value: 'stable' },
|
|
1954
|
+
{ name: 'Keep on main (next)', value: 'next' },
|
|
1955
|
+
],
|
|
1956
|
+
default: 'stable',
|
|
1957
|
+
});
|
|
1958
|
+
if (chosen === 'next') channelOptions.nextSet.add(code);
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Optional channel-switch offer. Fires only when the user opted in via
|
|
1963
|
+
// the gate above. 'keep' falls through to the existing per-channel
|
|
1964
|
+
// logic (which runs upgrade classification for stable). Any switch
|
|
1965
|
+
// records the new intent into channelOptions and skips upgrade prompts.
|
|
1966
|
+
if (reviewChannels && recordedChannel) {
|
|
1967
|
+
const switchChoices = [
|
|
1968
|
+
{
|
|
1969
|
+
name: `Keep on '${recordedChannel}'${prev.version ? ` @ ${prev.version}` : ''}`,
|
|
1970
|
+
value: 'keep',
|
|
1971
|
+
},
|
|
1972
|
+
];
|
|
1973
|
+
if (recordedChannel !== 'stable') {
|
|
1974
|
+
switchChoices.push({ name: 'Switch to stable (released version)', value: 'stable' });
|
|
1975
|
+
}
|
|
1976
|
+
if (recordedChannel !== 'next') {
|
|
1977
|
+
switchChoices.push({ name: 'Switch to next (main HEAD)', value: 'next' });
|
|
1978
|
+
}
|
|
1979
|
+
switchChoices.push({ name: 'Pin to a specific version tag', value: 'pin' });
|
|
1980
|
+
|
|
1981
|
+
const choice = await prompts.select({
|
|
1982
|
+
message: `${code} channel:`,
|
|
1983
|
+
choices: switchChoices,
|
|
1984
|
+
default: 'keep',
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
if (choice === 'next') {
|
|
1988
|
+
channelOptions.nextSet.add(code);
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
if (choice === 'pin') {
|
|
1992
|
+
const pinValue = await prompts.text({
|
|
1993
|
+
message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
|
|
1994
|
+
validate: (value) => {
|
|
1995
|
+
if (!value || !/^[\w.\-+/]+$/.test(String(value).trim())) {
|
|
1996
|
+
return 'Must be a non-empty tag name (letters, digits, dots, hyphens).';
|
|
1997
|
+
}
|
|
1998
|
+
},
|
|
1999
|
+
});
|
|
2000
|
+
channelOptions.pins.set(code, String(pinValue).trim());
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
if (choice === 'stable') {
|
|
2004
|
+
// Switch to stable: install at the top stable tag without an
|
|
2005
|
+
// upgrade-classification prompt (the user explicitly opted in).
|
|
2006
|
+
// Also warm the tag cache here so the actual clone step doesn't
|
|
2007
|
+
// need a second GitHub API call (can hit rate limits).
|
|
2008
|
+
if (parsed) {
|
|
2009
|
+
try {
|
|
2010
|
+
await fetchStableTags(parsed.owner, parsed.repo);
|
|
2011
|
+
} catch {
|
|
2012
|
+
// best effort; clone step will surface any failure
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
// 'keep' → fall through with recordedChannel below.
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
if (recordedChannel === 'pinned' || recordedChannel === 'next') {
|
|
2021
|
+
// Respect any explicit channel intent the user already expressed via
|
|
2022
|
+
// CLI flags (--channel / --all-* / --next=CODE / --pin CODE=TAG) or
|
|
2023
|
+
// via the interactive review gate above. Only auto-re-assert the
|
|
2024
|
+
// recorded channel when the user hasn't opted into anything else —
|
|
2025
|
+
// otherwise --all-stable (or a review "switch to stable") would be
|
|
2026
|
+
// silently clobbered by the prior channel.
|
|
2027
|
+
const alreadyDecided = channelOptions.global || channelOptions.nextSet.has(code) || channelOptions.pins.has(code);
|
|
2028
|
+
if (!alreadyDecided) {
|
|
2029
|
+
if (recordedChannel === 'pinned' && prev.version) {
|
|
2030
|
+
channelOptions.pins.set(code, prev.version);
|
|
2031
|
+
} else if (recordedChannel === 'next') {
|
|
2032
|
+
channelOptions.nextSet.add(code);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Stable channel: check for a newer released tag.
|
|
2039
|
+
if (!parsed) continue;
|
|
2040
|
+
// Respect explicit CLI intent (--pin / --next=CODE / --all-*) and any
|
|
2041
|
+
// choice the user already made in the earlier review gate. Without this
|
|
2042
|
+
// guard the upgrade classifier below would unconditionally call
|
|
2043
|
+
// `channelOptions.pins.set(code, prev.version)` on decline/major-refuse/
|
|
2044
|
+
// fetch-error, silently clobbering the user's override.
|
|
2045
|
+
const alreadyDecided = channelOptions.global || channelOptions.nextSet.has(code) || channelOptions.pins.has(code);
|
|
2046
|
+
if (alreadyDecided) continue;
|
|
2047
|
+
let tags;
|
|
2048
|
+
try {
|
|
2049
|
+
tags = await fetchStableTags(parsed.owner, parsed.repo);
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
await prompts.log.warn(`Could not check for updates on ${code} (${error.message}). Leaving at ${prev.version}.`);
|
|
2052
|
+
if (prev.version) channelOptions.pins.set(code, prev.version);
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
if (!tags || tags.length === 0) continue;
|
|
2056
|
+
const topTag = tags[0].tag; // e.g. "v1.7.0"
|
|
2057
|
+
const currentTag = prev.version || '';
|
|
2058
|
+
const diffClass = classifyUpgrade(currentTag, topTag);
|
|
2059
|
+
|
|
2060
|
+
if (diffClass === 'none') continue; // already at or above top tag
|
|
2061
|
+
|
|
2062
|
+
const notes = releaseNotesUrl(repoUrl, topTag);
|
|
2063
|
+
let accept;
|
|
2064
|
+
if (diffClass === 'major') {
|
|
2065
|
+
if (yes) {
|
|
2066
|
+
// Major under --yes is refused by design.
|
|
2067
|
+
await prompts.log.warn(
|
|
2068
|
+
`${code} ${currentTag} → ${topTag} is a new major release; staying on ${currentTag}. ` +
|
|
2069
|
+
`To accept, rerun with --pin ${code}=${topTag}.`,
|
|
2070
|
+
);
|
|
2071
|
+
channelOptions.pins.set(code, currentTag);
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
accept = await prompts.confirm({
|
|
2075
|
+
message:
|
|
2076
|
+
`${code} ${topTag} available — new major release (may change behavior).` +
|
|
2077
|
+
(notes ? ` Release notes: ${notes}.` : '') +
|
|
2078
|
+
' Upgrade?',
|
|
2079
|
+
default: false,
|
|
2080
|
+
});
|
|
2081
|
+
} else if (diffClass === 'minor') {
|
|
2082
|
+
if (yes) {
|
|
2083
|
+
accept = true;
|
|
2084
|
+
} else {
|
|
2085
|
+
accept = await prompts.confirm({
|
|
2086
|
+
message: `${code} ${topTag} available (new features).` + (notes ? ` Release notes: ${notes}.` : '') + ' Upgrade?',
|
|
2087
|
+
default: true,
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
} else {
|
|
2091
|
+
// patch
|
|
2092
|
+
if (yes) {
|
|
2093
|
+
accept = true;
|
|
2094
|
+
} else {
|
|
2095
|
+
accept = await prompts.confirm({
|
|
2096
|
+
message: `${code} ${topTag} available. Upgrade?`,
|
|
2097
|
+
default: true,
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
if (!accept && currentTag) {
|
|
2103
|
+
// Freeze the current version by pinning it for this run.
|
|
2104
|
+
channelOptions.pins.set(code, currentTag);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
1597
2108
|
}
|
|
1598
2109
|
|
|
1599
2110
|
module.exports = { UI };
|