bmad-method 6.3.1-next.2 → 6.3.1-next.21
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 -3
- 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-08-scoping.md +70 -23
- package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-11-polish.md +1 -1
- 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-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 +412 -1
- package/src/bmm-skills/4-implementation/bmad-create-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-retrospective/SKILL.md +1507 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/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 +3 -0
- package/src/scripts/resolve_config.py +176 -0
- package/src/scripts/resolve_customization.py +230 -0
- package/tools/installer/cli-utils.js +0 -137
- package/tools/installer/commands/install.js +13 -0
- package/tools/installer/commands/status.js +1 -1
- package/tools/installer/commands/uninstall.js +1 -1
- package/tools/installer/core/config.js +4 -1
- package/tools/installer/core/existing-install.js +1 -1
- package/tools/installer/core/install-paths.js +12 -6
- package/tools/installer/core/installer.js +182 -95
- package/tools/installer/core/manifest-generator.js +347 -190
- package/tools/installer/core/manifest.js +49 -642
- package/tools/installer/file-ops.js +1 -1
- package/tools/installer/fs-native.js +116 -0
- package/tools/installer/ide/_config-driven.js +1 -1
- package/tools/installer/ide/platform-codes.js +1 -1
- package/tools/installer/ide/shared/path-utils.js +0 -145
- package/tools/installer/ide/shared/skill-manifest.js +1 -1
- package/tools/installer/message-loader.js +1 -1
- 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 +131 -24
- package/tools/installer/modules/custom-module-manager.js +161 -47
- package/tools/installer/modules/external-manager.js +236 -73
- package/tools/installer/modules/official-modules.js +61 -63
- package/tools/installer/modules/plugin-resolver.js +1 -1
- package/tools/installer/modules/registry-client.js +133 -12
- package/tools/installer/modules/registry-fallback.yaml +8 -0
- package/tools/installer/modules/version-resolver.js +336 -0
- package/tools/installer/project-root.js +55 -1
- package/tools/installer/prompts.js +0 -106
- package/tools/installer/ui.js +457 -51
- package/tools/migrate-custom-module-paths.js +1 -1
- 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-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-qa-generate-e2e-tests/workflow.md +0 -136
- package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
- package/tools/installer/ide/shared/agent-command-generator.js +0 -180
- package/tools/installer/ide/shared/bmad-artifacts.js +0 -208
- package/tools/installer/ide/shared/module-injections.js +0 -136
- package/tools/installer/ide/templates/agent-command-template.md +0 -14
- package/tools/installer/ide/templates/combined/antigravity.md +0 -8
- package/tools/installer/ide/templates/combined/default-agent.md +0 -15
- package/tools/installer/ide/templates/combined/default-task.md +0 -10
- package/tools/installer/ide/templates/combined/default-tool.md +0 -10
- package/tools/installer/ide/templates/combined/default-workflow.md +0 -6
- package/tools/installer/ide/templates/combined/gemini-agent.toml +0 -14
- package/tools/installer/ide/templates/combined/gemini-task.toml +0 -11
- package/tools/installer/ide/templates/combined/gemini-tool.toml +0 -11
- package/tools/installer/ide/templates/combined/gemini-workflow-yaml.toml +0 -16
- package/tools/installer/ide/templates/combined/gemini-workflow.toml +0 -14
- package/tools/installer/ide/templates/combined/kiro-agent.md +0 -16
- package/tools/installer/ide/templates/combined/kiro-task.md +0 -9
- package/tools/installer/ide/templates/combined/kiro-tool.md +0 -9
- package/tools/installer/ide/templates/combined/kiro-workflow.md +0 -7
- package/tools/installer/ide/templates/combined/opencode-agent.md +0 -15
- package/tools/installer/ide/templates/combined/opencode-task.md +0 -13
- package/tools/installer/ide/templates/combined/opencode-tool.md +0 -13
- package/tools/installer/ide/templates/combined/opencode-workflow-yaml.md +0 -16
- package/tools/installer/ide/templates/combined/opencode-workflow.md +0 -16
- package/tools/installer/ide/templates/combined/rovodev.md +0 -9
- package/tools/installer/ide/templates/combined/trae.md +0 -9
- package/tools/installer/ide/templates/combined/windsurf-workflow.md +0 -10
- package/tools/installer/ide/templates/split/.gitkeep +0 -0
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
const fs = require('fs-
|
|
1
|
+
const fs = require('../fs-native');
|
|
2
2
|
const os = require('node:os');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
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
|
};
|
|
@@ -109,46 +166,6 @@ class ExternalModuleManager {
|
|
|
109
166
|
return modules.find((m) => m.code === code) || null;
|
|
110
167
|
}
|
|
111
168
|
|
|
112
|
-
/**
|
|
113
|
-
* Get module info by key
|
|
114
|
-
* @param {string} key - The module key (e.g., 'bmad-creative-intelligence-suite')
|
|
115
|
-
* @returns {Object|null} Module info or null if not found
|
|
116
|
-
*/
|
|
117
|
-
async getModuleByKey(key) {
|
|
118
|
-
const modules = await this.listAvailable();
|
|
119
|
-
return modules.find((m) => m.key === key) || null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Check if a module code exists in external modules
|
|
124
|
-
* @param {string} code - The module code to check
|
|
125
|
-
* @returns {boolean} True if the module exists
|
|
126
|
-
*/
|
|
127
|
-
async hasModule(code) {
|
|
128
|
-
const module = await this.getModuleByCode(code);
|
|
129
|
-
return module !== null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Get the URL for a module by code
|
|
134
|
-
* @param {string} code - The module code
|
|
135
|
-
* @returns {string|null} The URL or null if not found
|
|
136
|
-
*/
|
|
137
|
-
async getModuleUrl(code) {
|
|
138
|
-
const module = await this.getModuleByCode(code);
|
|
139
|
-
return module ? module.url : null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Get the module definition path for a module by code
|
|
144
|
-
* @param {string} code - The module code
|
|
145
|
-
* @returns {string|null} The module definition path or null if not found
|
|
146
|
-
*/
|
|
147
|
-
async getModuleDefinition(code) {
|
|
148
|
-
const module = await this.getModuleByCode(code);
|
|
149
|
-
return module ? module.moduleDefinition : null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
169
|
/**
|
|
153
170
|
* Get the cache directory for external modules
|
|
154
171
|
* @returns {string} Path to the external modules cache directory
|
|
@@ -159,10 +176,15 @@ class ExternalModuleManager {
|
|
|
159
176
|
}
|
|
160
177
|
|
|
161
178
|
/**
|
|
162
|
-
* 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
|
+
*
|
|
163
182
|
* @param {string} moduleCode - Code of the external module
|
|
164
183
|
* @param {Object} options - Clone options
|
|
165
|
-
* @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').
|
|
166
188
|
* @returns {string} Path to the cloned repository
|
|
167
189
|
*/
|
|
168
190
|
async cloneExternalModule(moduleCode, options = {}) {
|
|
@@ -200,38 +222,160 @@ class ExternalModuleManager {
|
|
|
200
222
|
return await prompts.spinner();
|
|
201
223
|
};
|
|
202
224
|
|
|
203
|
-
//
|
|
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
|
+
|
|
204
334
|
let needsDependencyInstall = false;
|
|
205
335
|
let wasNewClone = false;
|
|
206
336
|
|
|
207
|
-
|
|
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
|
+
|
|
208
343
|
if (await fs.pathExists(moduleCacheDir)) {
|
|
209
|
-
//
|
|
344
|
+
// Cache exists on the right channel. Refresh the ref.
|
|
210
345
|
const fetchSpinner = await createSpinner();
|
|
211
346
|
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
|
|
212
347
|
try {
|
|
213
|
-
const
|
|
214
|
-
// Fetch and reset to remote - works better with shallow clones than pull
|
|
215
|
-
execSync('git fetch origin --depth 1', {
|
|
216
|
-
cwd: moduleCacheDir,
|
|
217
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
218
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
219
|
-
});
|
|
220
|
-
execSync('git reset --hard origin/HEAD', {
|
|
221
|
-
cwd: moduleCacheDir,
|
|
222
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
223
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
224
|
-
});
|
|
225
|
-
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();
|
|
226
349
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
});
|
|
231
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;
|
|
232
377
|
} catch {
|
|
233
378
|
fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`);
|
|
234
|
-
// If update fails, remove and re-clone
|
|
235
379
|
await fs.remove(moduleCacheDir);
|
|
236
380
|
wasNewClone = true;
|
|
237
381
|
}
|
|
@@ -239,22 +383,41 @@ class ExternalModuleManager {
|
|
|
239
383
|
wasNewClone = true;
|
|
240
384
|
}
|
|
241
385
|
|
|
242
|
-
// Clone if not exists or was removed
|
|
243
386
|
if (wasNewClone) {
|
|
244
387
|
const fetchSpinner = await createSpinner();
|
|
245
388
|
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
|
|
246
389
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
|
251
401
|
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
|
|
252
402
|
} catch (error) {
|
|
253
403
|
fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`);
|
|
254
|
-
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}`);
|
|
255
405
|
}
|
|
256
406
|
}
|
|
257
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
|
+
|
|
258
421
|
// Install dependencies if package.json exists
|
|
259
422
|
const packageJsonPath = path.join(moduleCacheDir, 'package.json');
|
|
260
423
|
const nodeModulesPath = path.join(moduleCacheDir, 'node_modules');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
|
-
const fs = require('fs-
|
|
2
|
+
const fs = require('../fs-native');
|
|
3
3
|
const yaml = require('yaml');
|
|
4
4
|
const prompts = require('../prompts');
|
|
5
5
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');
|
|
@@ -12,7 +12,14 @@ class OfficialModules {
|
|
|
12
12
|
// Config collection state (merged from ConfigCollector)
|
|
13
13
|
this.collectedConfig = {};
|
|
14
14
|
this._existingConfig = null;
|
|
15
|
+
// Tracked during interactive config collection so {directory_name}
|
|
16
|
+
// placeholder defaults can be resolved in buildQuestion().
|
|
15
17
|
this.currentProjectDir = null;
|
|
18
|
+
// Install-time channel flag state. Set by Config.build once, then used as
|
|
19
|
+
// the default for every findModuleSource/cloneExternalModule call so that
|
|
20
|
+
// pre-install config collection and the install step agree on which ref
|
|
21
|
+
// to clone.
|
|
22
|
+
this.channelOptions = options.channelOptions || null;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -36,7 +43,7 @@ class OfficialModules {
|
|
|
36
43
|
* @returns {OfficialModules}
|
|
37
44
|
*/
|
|
38
45
|
static async build(config, paths) {
|
|
39
|
-
const instance = new OfficialModules();
|
|
46
|
+
const instance = new OfficialModules({ channelOptions: config.channelOptions });
|
|
40
47
|
|
|
41
48
|
// Pre-collected by UI or quickUpdate — store and load existing for path-change detection
|
|
42
49
|
if (config.moduleConfigs) {
|
|
@@ -194,6 +201,12 @@ class OfficialModules {
|
|
|
194
201
|
* @returns {string|null} Path to the module source or null if not found
|
|
195
202
|
*/
|
|
196
203
|
async findModuleSource(moduleCode, options = {}) {
|
|
204
|
+
// Inherit channelOptions from the install-scoped instance when the caller
|
|
205
|
+
// didn't pass one explicitly. Keeps pre-install config collection and the
|
|
206
|
+
// actual install step looking at the same git ref.
|
|
207
|
+
if (options.channelOptions === undefined && this.channelOptions) {
|
|
208
|
+
options = { ...options, channelOptions: this.channelOptions };
|
|
209
|
+
}
|
|
197
210
|
const projectRoot = getProjectRoot();
|
|
198
211
|
|
|
199
212
|
// Check for core module (directly under src/core-skills)
|
|
@@ -212,13 +225,13 @@ class OfficialModules {
|
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
// Check external official modules
|
|
228
|
+
// Check external official modules (pass channelOptions so channel plan applies)
|
|
216
229
|
const externalSource = await this.externalModuleManager.findExternalModuleSource(moduleCode, options);
|
|
217
230
|
if (externalSource) {
|
|
218
231
|
return externalSource;
|
|
219
232
|
}
|
|
220
233
|
|
|
221
|
-
// Check community modules
|
|
234
|
+
// Check community modules (pass channelOptions for --next/--pin overrides)
|
|
222
235
|
const { CommunityModuleManager } = require('./community-manager');
|
|
223
236
|
const communityMgr = new CommunityModuleManager();
|
|
224
237
|
const communitySource = await communityMgr.findModuleSource(moduleCode, options);
|
|
@@ -256,7 +269,10 @@ class OfficialModules {
|
|
|
256
269
|
return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
|
|
257
270
|
}
|
|
258
271
|
|
|
259
|
-
const sourcePath = await this.findModuleSource(moduleName, {
|
|
272
|
+
const sourcePath = await this.findModuleSource(moduleName, {
|
|
273
|
+
silent: options.silent,
|
|
274
|
+
channelOptions: options.channelOptions,
|
|
275
|
+
});
|
|
260
276
|
const targetPath = path.join(bmadDir, moduleName);
|
|
261
277
|
|
|
262
278
|
if (!sourcePath) {
|
|
@@ -279,11 +295,24 @@ class OfficialModules {
|
|
|
279
295
|
const manifestObj = new Manifest();
|
|
280
296
|
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
|
281
297
|
|
|
298
|
+
// Pick up channel resolution recorded by whichever manager did the clone.
|
|
299
|
+
const externalResolution = this.externalModuleManager.getResolution(moduleName);
|
|
300
|
+
let communityResolution = null;
|
|
301
|
+
if (!externalResolution) {
|
|
302
|
+
const { CommunityModuleManager } = require('./community-manager');
|
|
303
|
+
communityResolution = new CommunityModuleManager().getResolution(moduleName);
|
|
304
|
+
}
|
|
305
|
+
const resolution = externalResolution || communityResolution;
|
|
306
|
+
|
|
282
307
|
await manifestObj.addModule(bmadDir, moduleName, {
|
|
283
|
-
version: versionInfo.version,
|
|
308
|
+
version: resolution?.version || versionInfo.version,
|
|
284
309
|
source: versionInfo.source,
|
|
285
310
|
npmPackage: versionInfo.npmPackage,
|
|
286
311
|
repoUrl: versionInfo.repoUrl,
|
|
312
|
+
channel: resolution?.channel,
|
|
313
|
+
sha: resolution?.sha,
|
|
314
|
+
registryApprovedTag: communityResolution?.registryApprovedTag,
|
|
315
|
+
registryApprovedSha: communityResolution?.registryApprovedSha,
|
|
287
316
|
});
|
|
288
317
|
|
|
289
318
|
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
|
@@ -331,18 +360,37 @@ class OfficialModules {
|
|
|
331
360
|
await this.createModuleDirectories(resolved.code, bmadDir, options);
|
|
332
361
|
}
|
|
333
362
|
|
|
334
|
-
// Update manifest
|
|
363
|
+
// Update manifest. For custom modules, derive channel from the git ref:
|
|
364
|
+
// cloneRef present → pinned at that ref
|
|
365
|
+
// cloneRef absent → next (main HEAD)
|
|
366
|
+
// local path → no channel concept
|
|
335
367
|
const { Manifest } = require('../core/manifest');
|
|
336
368
|
const manifestObj = new Manifest();
|
|
337
369
|
|
|
338
|
-
|
|
339
|
-
|
|
370
|
+
const hasGitClone = !!resolved.repoUrl;
|
|
371
|
+
const manifestEntry = {
|
|
372
|
+
version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null),
|
|
340
373
|
source: 'custom',
|
|
341
374
|
npmPackage: null,
|
|
342
375
|
repoUrl: resolved.repoUrl || null,
|
|
343
|
-
}
|
|
376
|
+
};
|
|
377
|
+
if (hasGitClone) {
|
|
378
|
+
manifestEntry.channel = resolved.cloneRef ? 'pinned' : 'next';
|
|
379
|
+
if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
|
|
380
|
+
if (resolved.rawInput) manifestEntry.rawSource = resolved.rawInput;
|
|
381
|
+
}
|
|
382
|
+
if (resolved.localPath) manifestEntry.localPath = resolved.localPath;
|
|
383
|
+
await manifestObj.addModule(bmadDir, resolved.code, manifestEntry);
|
|
344
384
|
|
|
345
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
success: true,
|
|
387
|
+
module: resolved.code,
|
|
388
|
+
path: targetPath,
|
|
389
|
+
// Match the manifestEntry.version expression above so downstream summary
|
|
390
|
+
// lines show the cloned ref (tag or 'main') instead of the on-disk
|
|
391
|
+
// package.json version for git-backed custom installs.
|
|
392
|
+
versionInfo: { version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || '') },
|
|
393
|
+
};
|
|
346
394
|
}
|
|
347
395
|
|
|
348
396
|
/**
|
|
@@ -500,32 +548,6 @@ class OfficialModules {
|
|
|
500
548
|
}
|
|
501
549
|
}
|
|
502
550
|
|
|
503
|
-
/**
|
|
504
|
-
* Find all .md agent files recursively in a directory
|
|
505
|
-
* @param {string} dir - Directory to search
|
|
506
|
-
* @returns {Array} List of .md agent file paths
|
|
507
|
-
*/
|
|
508
|
-
async findAgentMdFiles(dir) {
|
|
509
|
-
const agentFiles = [];
|
|
510
|
-
|
|
511
|
-
async function searchDirectory(searchDir) {
|
|
512
|
-
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
|
513
|
-
|
|
514
|
-
for (const entry of entries) {
|
|
515
|
-
const fullPath = path.join(searchDir, entry.name);
|
|
516
|
-
|
|
517
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
518
|
-
agentFiles.push(fullPath);
|
|
519
|
-
} else if (entry.isDirectory()) {
|
|
520
|
-
await searchDirectory(fullPath);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
await searchDirectory(dir);
|
|
526
|
-
return agentFiles;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
551
|
/**
|
|
530
552
|
* Create directories declared in module.yaml's `directories` key
|
|
531
553
|
* This replaces the security-risky module installer pattern with declarative config
|
|
@@ -699,29 +721,6 @@ class OfficialModules {
|
|
|
699
721
|
return { createdDirs, movedDirs, createdWdsFolders };
|
|
700
722
|
}
|
|
701
723
|
|
|
702
|
-
/**
|
|
703
|
-
* Private: Process module configuration
|
|
704
|
-
* @param {string} modulePath - Path to installed module
|
|
705
|
-
* @param {string} moduleName - Module name
|
|
706
|
-
*/
|
|
707
|
-
async processModuleConfig(modulePath, moduleName) {
|
|
708
|
-
const configPath = path.join(modulePath, 'config.yaml');
|
|
709
|
-
|
|
710
|
-
if (await fs.pathExists(configPath)) {
|
|
711
|
-
try {
|
|
712
|
-
let configContent = await fs.readFile(configPath, 'utf8');
|
|
713
|
-
|
|
714
|
-
// Replace path placeholders
|
|
715
|
-
configContent = configContent.replaceAll('{project-root}', `bmad/${moduleName}`);
|
|
716
|
-
configContent = configContent.replaceAll('{module}', moduleName);
|
|
717
|
-
|
|
718
|
-
await fs.writeFile(configPath, configContent, 'utf8');
|
|
719
|
-
} catch (error) {
|
|
720
|
-
await prompts.log.warn(`Failed to process module config: ${error.message}`);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
724
|
/**
|
|
726
725
|
* Private: Sync module files (preserving user modifications)
|
|
727
726
|
* @param {string} sourcePath - Source module path
|
|
@@ -867,10 +866,10 @@ class OfficialModules {
|
|
|
867
866
|
let foundAny = false;
|
|
868
867
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
869
868
|
|
|
869
|
+
const nonModuleDirs = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom']);
|
|
870
870
|
for (const entry of entries) {
|
|
871
871
|
if (entry.isDirectory()) {
|
|
872
|
-
|
|
873
|
-
if (entry.name === '_config' || entry.name === '_memory') {
|
|
872
|
+
if (nonModuleDirs.has(entry.name)) {
|
|
874
873
|
continue;
|
|
875
874
|
}
|
|
876
875
|
|
|
@@ -1091,7 +1090,6 @@ class OfficialModules {
|
|
|
1091
1090
|
*/
|
|
1092
1091
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
|
1093
1092
|
this.currentProjectDir = projectDir;
|
|
1094
|
-
|
|
1095
1093
|
// Load existing config if not already loaded
|
|
1096
1094
|
if (!this._existingConfig) {
|
|
1097
1095
|
await this.loadExistingConfig(projectDir);
|