bmad-method 6.2.2 → 6.2.3-next.1
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/.claude-plugin/marketplace.json +78 -0
- package/package.json +8 -8
- package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
- package/src/core-skills/bmad-init/scripts/bmad_init.py +35 -4
- package/src/core-skills/bmad-init/scripts/tests/test_bmad_init.py +64 -0
- package/tools/{cli → installer}/bmad-cli.js +3 -1
- package/tools/{cli/lib → installer}/cli-utils.js +3 -4
- package/tools/{cli → installer}/commands/install.js +3 -3
- package/tools/{cli → installer}/commands/status.js +4 -4
- package/tools/{cli → installer}/commands/uninstall.js +5 -5
- package/tools/installer/core/config.js +52 -0
- package/tools/{cli/installers/lib → installer}/core/custom-module-cache.js +1 -1
- package/tools/installer/core/existing-install.js +127 -0
- package/tools/installer/core/install-paths.js +129 -0
- package/tools/installer/core/installer.js +1790 -0
- package/tools/{cli/installers/lib → installer}/core/manifest-generator.js +3 -3
- package/tools/{cli/installers/lib → installer}/core/manifest.js +2 -2
- package/tools/{cli/installers/lib/custom/handler.js → installer/custom-handler.js} +1 -1
- package/tools/{cli/installers/lib → installer}/ide/_config-driven.js +30 -397
- package/tools/{cli/installers/lib → installer}/ide/manager.js +1 -53
- package/tools/installer/ide/platform-codes.js +37 -0
- package/tools/installer/ide/platform-codes.yaml +190 -0
- package/tools/{cli/installers/lib → installer}/ide/shared/module-injections.js +1 -1
- package/tools/{cli/installers/lib → installer}/message-loader.js +2 -2
- package/tools/installer/modules/custom-modules.js +197 -0
- package/tools/installer/modules/external-manager.js +323 -0
- package/tools/{cli/installers/lib/core/config-collector.js → installer/modules/official-modules.js} +714 -43
- package/tools/{cli/lib → installer}/ui.js +65 -299
- package/tools/javascript-conventions.md +5 -0
- package/tools/bmad-npx-wrapper.js +0 -38
- package/tools/cli/installers/lib/core/dependency-resolver.js +0 -743
- package/tools/cli/installers/lib/core/detector.js +0 -223
- package/tools/cli/installers/lib/core/ide-config-manager.js +0 -157
- package/tools/cli/installers/lib/core/installer.js +0 -3002
- package/tools/cli/installers/lib/ide/_base-ide.js +0 -657
- package/tools/cli/installers/lib/ide/platform-codes.js +0 -100
- package/tools/cli/installers/lib/ide/platform-codes.yaml +0 -341
- package/tools/cli/installers/lib/modules/external-manager.js +0 -136
- package/tools/cli/installers/lib/modules/manager.js +0 -928
- package/tools/cli/lib/config.js +0 -213
- package/tools/cli/lib/platform-codes.js +0 -116
- package/tools/lib/xml-utils.js +0 -13
- /package/tools/{cli → installer}/README.md +0 -0
- /package/tools/{cli → installer}/external-official-modules.yaml +0 -0
- /package/tools/{cli/lib → installer}/file-ops.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/agent-command-generator.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/bmad-artifacts.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/path-utils.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/shared/skill-manifest.js +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/agent-command-template.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/antigravity.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-agent.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-task.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-tool.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow-yaml.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow.toml +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-agent.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-task.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-tool.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow-yaml.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/rovodev.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/trae.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/combined/windsurf-workflow.md +0 -0
- /package/tools/{cli/installers/lib → installer}/ide/templates/split/.gitkeep +0 -0
- /package/tools/{cli/installers → installer}/install-messages.yaml +0 -0
- /package/tools/{cli/lib → installer}/project-root.js +0 -0
- /package/tools/{cli/lib → installer}/prompts.js +0 -0
- /package/tools/{cli/lib → installer}/yaml-format.js +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bmad-method",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Brian (BMad) Madison"
|
|
5
|
+
},
|
|
6
|
+
"description": "Breakthrough Method of Agile AI-driven Development — a full-lifecycle framework with agents and workflows for analysis, planning, architecture, and implementation.",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/bmad-code-org/BMAD-METHOD",
|
|
9
|
+
"repository": "https://github.com/bmad-code-org/BMAD-METHOD",
|
|
10
|
+
"keywords": ["bmad", "agile", "ai", "orchestrator", "development", "methodology", "agents"],
|
|
11
|
+
"plugins": [
|
|
12
|
+
{
|
|
13
|
+
"name": "bmad-pro-skills",
|
|
14
|
+
"source": "./",
|
|
15
|
+
"description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
|
|
16
|
+
"version": "6.3.0",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Brian (BMad) Madison"
|
|
19
|
+
},
|
|
20
|
+
"skills": [
|
|
21
|
+
"./src/core-skills/bmad-help",
|
|
22
|
+
"./src/core-skills/bmad-init",
|
|
23
|
+
"./src/core-skills/bmad-brainstorming",
|
|
24
|
+
"./src/core-skills/bmad-distillator",
|
|
25
|
+
"./src/core-skills/bmad-party-mode",
|
|
26
|
+
"./src/core-skills/bmad-shard-doc",
|
|
27
|
+
"./src/core-skills/bmad-advanced-elicitation",
|
|
28
|
+
"./src/core-skills/bmad-editorial-review-prose",
|
|
29
|
+
"./src/core-skills/bmad-editorial-review-structure",
|
|
30
|
+
"./src/core-skills/bmad-index-docs",
|
|
31
|
+
"./src/core-skills/bmad-review-adversarial-general",
|
|
32
|
+
"./src/core-skills/bmad-review-edge-case-hunter"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "bmad-method-lifecycle",
|
|
37
|
+
"source": "./",
|
|
38
|
+
"description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
|
|
39
|
+
"version": "6.3.0",
|
|
40
|
+
"author": {
|
|
41
|
+
"name": "Brian (BMad) Madison"
|
|
42
|
+
},
|
|
43
|
+
"skills": [
|
|
44
|
+
"./src/bmm-skills/1-analysis/bmad-product-brief",
|
|
45
|
+
"./src/bmm-skills/1-analysis/bmad-agent-analyst",
|
|
46
|
+
"./src/bmm-skills/1-analysis/bmad-agent-tech-writer",
|
|
47
|
+
"./src/bmm-skills/1-analysis/bmad-document-project",
|
|
48
|
+
"./src/bmm-skills/1-analysis/research/bmad-domain-research",
|
|
49
|
+
"./src/bmm-skills/1-analysis/research/bmad-market-research",
|
|
50
|
+
"./src/bmm-skills/1-analysis/research/bmad-technical-research",
|
|
51
|
+
"./src/bmm-skills/2-plan-workflows/bmad-agent-pm",
|
|
52
|
+
"./src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer",
|
|
53
|
+
"./src/bmm-skills/2-plan-workflows/bmad-create-prd",
|
|
54
|
+
"./src/bmm-skills/2-plan-workflows/bmad-edit-prd",
|
|
55
|
+
"./src/bmm-skills/2-plan-workflows/bmad-validate-prd",
|
|
56
|
+
"./src/bmm-skills/2-plan-workflows/bmad-create-ux-design",
|
|
57
|
+
"./src/bmm-skills/3-solutioning/bmad-agent-architect",
|
|
58
|
+
"./src/bmm-skills/3-solutioning/bmad-create-architecture",
|
|
59
|
+
"./src/bmm-skills/3-solutioning/bmad-check-implementation-readiness",
|
|
60
|
+
"./src/bmm-skills/3-solutioning/bmad-create-epics-and-stories",
|
|
61
|
+
"./src/bmm-skills/3-solutioning/bmad-generate-project-context",
|
|
62
|
+
"./src/bmm-skills/4-implementation/bmad-agent-dev",
|
|
63
|
+
"./src/bmm-skills/4-implementation/bmad-agent-sm",
|
|
64
|
+
"./src/bmm-skills/4-implementation/bmad-agent-qa",
|
|
65
|
+
"./src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev",
|
|
66
|
+
"./src/bmm-skills/4-implementation/bmad-dev-story",
|
|
67
|
+
"./src/bmm-skills/4-implementation/bmad-quick-dev",
|
|
68
|
+
"./src/bmm-skills/4-implementation/bmad-sprint-planning",
|
|
69
|
+
"./src/bmm-skills/4-implementation/bmad-sprint-status",
|
|
70
|
+
"./src/bmm-skills/4-implementation/bmad-code-review",
|
|
71
|
+
"./src/bmm-skills/4-implementation/bmad-create-story",
|
|
72
|
+
"./src/bmm-skills/4-implementation/bmad-correct-course",
|
|
73
|
+
"./src/bmm-skills/4-implementation/bmad-retrospective",
|
|
74
|
+
"./src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-method",
|
|
4
|
-
"version": "6.2.
|
|
4
|
+
"version": "6.2.3-next.1",
|
|
5
5
|
"description": "Breakthrough Method of Agile AI-driven Development",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agile",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
},
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"author": "Brian (BMad) Madison",
|
|
21
|
-
"main": "tools/
|
|
21
|
+
"main": "tools/installer/bmad-cli.js",
|
|
22
22
|
"bin": {
|
|
23
|
-
"bmad": "tools/bmad-
|
|
24
|
-
"bmad-method": "tools/bmad-
|
|
23
|
+
"bmad": "tools/installer/bmad-cli.js",
|
|
24
|
+
"bmad-method": "tools/installer/bmad-cli.js"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"bmad:install": "node tools/
|
|
28
|
-
"bmad:uninstall": "node tools/
|
|
27
|
+
"bmad:install": "node tools/installer/bmad-cli.js install",
|
|
28
|
+
"bmad:uninstall": "node tools/installer/bmad-cli.js uninstall",
|
|
29
29
|
"docs:build": "node tools/build-docs.mjs",
|
|
30
30
|
"docs:dev": "astro dev --root website",
|
|
31
31
|
"docs:fix-links": "node tools/fix-doc-links.js",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"format:check": "prettier --check \"**/*.{js,cjs,mjs,json,yaml}\"",
|
|
35
35
|
"format:fix": "prettier --write \"**/*.{js,cjs,mjs,json,yaml}\"",
|
|
36
36
|
"format:fix:staged": "prettier --write",
|
|
37
|
-
"install:bmad": "node tools/
|
|
37
|
+
"install:bmad": "node tools/installer/bmad-cli.js install",
|
|
38
38
|
"lint": "eslint . --ext .js,.cjs,.mjs,.yaml --max-warnings=0",
|
|
39
39
|
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
|
40
40
|
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
|
41
41
|
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
|
42
42
|
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs && npm run validate:skills",
|
|
43
|
-
"rebundle": "node tools/
|
|
43
|
+
"rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
|
|
44
44
|
"test": "npm run test:refs && npm run test:install && npm run lint && npm run lint:md && npm run format:check",
|
|
45
45
|
"test:install": "node test/test-installation-components.js",
|
|
46
46
|
"test:refs": "node test/test-file-refs-csv.js",
|
|
@@ -172,7 +172,7 @@ parts: 1
|
|
|
172
172
|
- Deferred: CI/CD integration, telemetry for module authors, air-gapped enterprise install, zip bundle integrity verification (checksums/signing), deeper non-technical platform integrations
|
|
173
173
|
|
|
174
174
|
## Current Installer (migration context)
|
|
175
|
-
- Entry: `tools/
|
|
175
|
+
- Entry: `tools/installer/bmad-cli.js` (Commander.js) → `tools/installer/core/installer.js`
|
|
176
176
|
- Platforms: `platform-codes.yaml` (~20 platforms with target dirs, legacy dirs, template types, special flags)
|
|
177
177
|
- Manifests: CSV files (skill/workflow/agent-manifest.csv) are current source of truth, not JSON
|
|
178
178
|
- External modules: `external-official-modules.yaml` (CIS, GDS, TEA, WDS) from npm with semver
|
|
@@ -166,9 +166,27 @@ def resolve_project_root_placeholder(value, project_root):
|
|
|
166
166
|
"""Replace {project-root} placeholder with actual path."""
|
|
167
167
|
if not value or not isinstance(value, str):
|
|
168
168
|
return value
|
|
169
|
-
if '{project-root}' in value:
|
|
170
|
-
return value
|
|
171
|
-
|
|
169
|
+
if '{project-root}' not in value:
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
# Strip the {project-root} token to inspect what remains, so we can
|
|
173
|
+
# correctly handle absolute paths stored as "{project-root}//absolute/path"
|
|
174
|
+
# (produced by the "{project-root}/{value}" template applied to an absolute value).
|
|
175
|
+
suffix = value.replace('{project-root}', '', 1)
|
|
176
|
+
|
|
177
|
+
# Strip the one path separator that follows the token (if any)
|
|
178
|
+
if suffix.startswith('/') or suffix.startswith('\\'):
|
|
179
|
+
remainder = suffix[1:]
|
|
180
|
+
else:
|
|
181
|
+
remainder = suffix
|
|
182
|
+
|
|
183
|
+
if os.path.isabs(remainder):
|
|
184
|
+
# The original value was an absolute path stored with a {project-root}/ prefix.
|
|
185
|
+
# Return the absolute path directly — no joining needed.
|
|
186
|
+
return remainder
|
|
187
|
+
|
|
188
|
+
# Relative path: join with project root and normalize to resolve any .. segments.
|
|
189
|
+
return os.path.normpath(os.path.join(str(project_root), remainder))
|
|
172
190
|
|
|
173
191
|
|
|
174
192
|
def parse_var_specs(vars_string):
|
|
@@ -222,9 +240,22 @@ def apply_result_template(var_def, raw_value, context):
|
|
|
222
240
|
if not result_template:
|
|
223
241
|
return raw_value
|
|
224
242
|
|
|
243
|
+
# If the user supplied an absolute path and the template would prefix it with
|
|
244
|
+
# "{project-root}/", skip the template entirely to avoid producing a broken path
|
|
245
|
+
# like "/my/project//absolute/path".
|
|
246
|
+
if isinstance(raw_value, str) and os.path.isabs(raw_value):
|
|
247
|
+
return raw_value
|
|
248
|
+
|
|
225
249
|
ctx = dict(context)
|
|
226
250
|
ctx['value'] = raw_value
|
|
227
|
-
|
|
251
|
+
result = expand_template(result_template, ctx)
|
|
252
|
+
|
|
253
|
+
# Normalize the resulting path to resolve any ".." segments (e.g. when the user
|
|
254
|
+
# entered a relative path such as "../../outside-dir").
|
|
255
|
+
if isinstance(result, str) and '{' not in result and os.path.isabs(result):
|
|
256
|
+
result = os.path.normpath(result)
|
|
257
|
+
|
|
258
|
+
return result
|
|
228
259
|
|
|
229
260
|
|
|
230
261
|
# =============================================================================
|
|
@@ -110,6 +110,37 @@ class TestResolveProjectRootPlaceholder(unittest.TestCase):
|
|
|
110
110
|
def test_non_string(self):
|
|
111
111
|
self.assertEqual(resolve_project_root_placeholder(42, Path('/test')), 42)
|
|
112
112
|
|
|
113
|
+
def test_absolute_path_stored_with_prefix(self):
|
|
114
|
+
"""Absolute output_folder entered by user is stored as '{project-root}//abs/path'
|
|
115
|
+
by the '{project-root}/{value}' template. It must resolve to '/abs/path', not
|
|
116
|
+
'/project//abs/path'."""
|
|
117
|
+
result = resolve_project_root_placeholder(
|
|
118
|
+
'{project-root}//Users/me/outside', Path('/Users/me/myproject')
|
|
119
|
+
)
|
|
120
|
+
self.assertEqual(result, '/Users/me/outside')
|
|
121
|
+
|
|
122
|
+
def test_relative_path_with_traversal_is_normalized(self):
|
|
123
|
+
"""A relative path like '../../sibling' produces '{project-root}/../../sibling'
|
|
124
|
+
after the template. It must resolve to the normalized absolute path, not the
|
|
125
|
+
un-normalized string '/project/../../sibling'."""
|
|
126
|
+
result = resolve_project_root_placeholder(
|
|
127
|
+
'{project-root}/../../sibling', Path('/Users/me/myproject')
|
|
128
|
+
)
|
|
129
|
+
self.assertEqual(result, '/Users/sibling')
|
|
130
|
+
|
|
131
|
+
def test_relative_path_one_level_up(self):
|
|
132
|
+
result = resolve_project_root_placeholder(
|
|
133
|
+
'{project-root}/../outside-outputs', Path('/project/root')
|
|
134
|
+
)
|
|
135
|
+
self.assertEqual(result, '/project/outside-outputs')
|
|
136
|
+
|
|
137
|
+
def test_standard_relative_path_unchanged(self):
|
|
138
|
+
"""Normal in-project relative paths continue to work correctly."""
|
|
139
|
+
result = resolve_project_root_placeholder(
|
|
140
|
+
'{project-root}/_bmad-output', Path('/project/root')
|
|
141
|
+
)
|
|
142
|
+
self.assertEqual(result, '/project/root/_bmad-output')
|
|
143
|
+
|
|
113
144
|
|
|
114
145
|
class TestExpandTemplate(unittest.TestCase):
|
|
115
146
|
|
|
@@ -147,6 +178,39 @@ class TestApplyResultTemplate(unittest.TestCase):
|
|
|
147
178
|
result = apply_result_template(var_def, 'English', {})
|
|
148
179
|
self.assertEqual(result, 'English')
|
|
149
180
|
|
|
181
|
+
def test_absolute_value_skips_project_root_template(self):
|
|
182
|
+
"""When the user enters an absolute path, the '{project-root}/{value}' template
|
|
183
|
+
must not be applied — doing so would produce '/project//absolute/path'."""
|
|
184
|
+
var_def = {'result': '{project-root}/{value}'}
|
|
185
|
+
result = apply_result_template(
|
|
186
|
+
var_def, '/Users/me/shared-outputs', {'project-root': '/Users/me/myproject'}
|
|
187
|
+
)
|
|
188
|
+
self.assertEqual(result, '/Users/me/shared-outputs')
|
|
189
|
+
|
|
190
|
+
def test_relative_traversal_value_is_normalized(self):
|
|
191
|
+
"""A relative path like '../../outside' combined with the project-root template
|
|
192
|
+
must produce a clean normalized absolute path, not '/project/../../outside'."""
|
|
193
|
+
var_def = {'result': '{project-root}/{value}'}
|
|
194
|
+
result = apply_result_template(
|
|
195
|
+
var_def, '../../outside-dir', {'project-root': '/Users/me/myproject'}
|
|
196
|
+
)
|
|
197
|
+
self.assertEqual(result, '/Users/outside-dir')
|
|
198
|
+
|
|
199
|
+
def test_relative_one_level_up_is_normalized(self):
|
|
200
|
+
var_def = {'result': '{project-root}/{value}'}
|
|
201
|
+
result = apply_result_template(
|
|
202
|
+
var_def, '../sibling-outputs', {'project-root': '/project/root'}
|
|
203
|
+
)
|
|
204
|
+
self.assertEqual(result, '/project/sibling-outputs')
|
|
205
|
+
|
|
206
|
+
def test_normal_relative_value_unchanged(self):
|
|
207
|
+
"""Standard in-project relative paths still produce the expected joined path."""
|
|
208
|
+
var_def = {'result': '{project-root}/{value}'}
|
|
209
|
+
result = apply_result_template(
|
|
210
|
+
var_def, '_bmad-output', {'project-root': '/project/root'}
|
|
211
|
+
)
|
|
212
|
+
self.assertEqual(result, '/project/root/_bmad-output')
|
|
213
|
+
|
|
150
214
|
|
|
151
215
|
class TestLoadModuleYaml(unittest.TestCase):
|
|
152
216
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
1
3
|
const { program } = require('commander');
|
|
2
4
|
const path = require('node:path');
|
|
3
5
|
const fs = require('node:fs');
|
|
4
6
|
const { execSync } = require('node:child_process');
|
|
5
7
|
const semver = require('semver');
|
|
6
|
-
const prompts = require('./
|
|
8
|
+
const prompts = require('./prompts');
|
|
7
9
|
|
|
8
10
|
// The installer flow uses many sequential @clack/prompts, each adding keypress
|
|
9
11
|
// listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings.
|
|
@@ -8,7 +8,7 @@ const CLIUtils = {
|
|
|
8
8
|
*/
|
|
9
9
|
getVersion() {
|
|
10
10
|
try {
|
|
11
|
-
const packageJson = require(path.join(__dirname, '..', '..', '
|
|
11
|
+
const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
|
|
12
12
|
return packageJson.version || 'Unknown';
|
|
13
13
|
} catch {
|
|
14
14
|
return 'Unknown';
|
|
@@ -16,10 +16,9 @@ const CLIUtils = {
|
|
|
16
16
|
},
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Display BMAD logo using @clack intro + box
|
|
20
|
-
* @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen)
|
|
19
|
+
* Display BMAD logo and version using @clack intro + box
|
|
21
20
|
*/
|
|
22
|
-
async displayLogo(
|
|
21
|
+
async displayLogo() {
|
|
23
22
|
const version = this.getVersion();
|
|
24
23
|
const color = await prompts.getColor();
|
|
25
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
|
-
const prompts = require('../
|
|
3
|
-
const { Installer } = require('../
|
|
4
|
-
const { UI } = require('../
|
|
2
|
+
const prompts = require('../prompts');
|
|
3
|
+
const { Installer } = require('../core/installer');
|
|
4
|
+
const { UI } = require('../ui');
|
|
5
5
|
|
|
6
6
|
const installer = new Installer();
|
|
7
7
|
const ui = new UI();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
|
-
const prompts = require('../
|
|
3
|
-
const { Installer } = require('../
|
|
4
|
-
const { Manifest } = require('../
|
|
5
|
-
const { UI } = require('../
|
|
2
|
+
const prompts = require('../prompts');
|
|
3
|
+
const { Installer } = require('../core/installer');
|
|
4
|
+
const { Manifest } = require('../core/manifest');
|
|
5
|
+
const { UI } = require('../ui');
|
|
6
6
|
|
|
7
7
|
const installer = new Installer();
|
|
8
8
|
const manifest = new Manifest();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
|
-
const prompts = require('../
|
|
4
|
-
const { Installer } = require('../
|
|
3
|
+
const prompts = require('../prompts');
|
|
4
|
+
const { Installer } = require('../core/installer');
|
|
5
5
|
|
|
6
6
|
const installer = new Installer();
|
|
7
7
|
|
|
@@ -62,9 +62,9 @@ module.exports = {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const existingInstall = await installer.getStatus(projectDir);
|
|
65
|
-
const version = existingInstall.version
|
|
66
|
-
const modules =
|
|
67
|
-
const ides =
|
|
65
|
+
const version = existingInstall.installed ? existingInstall.version : 'unknown';
|
|
66
|
+
const modules = existingInstall.moduleIds.join(', ');
|
|
67
|
+
const ides = existingInstall.ides.join(', ');
|
|
68
68
|
|
|
69
69
|
const outputFolder = await installer.getOutputFolder(projectDir);
|
|
70
70
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean install configuration built from user input.
|
|
3
|
+
* User input comes from either UI answers or headless CLI flags.
|
|
4
|
+
*/
|
|
5
|
+
class Config {
|
|
6
|
+
constructor({ directory, modules, ides, skipPrompts, verbose, actionType, coreConfig, moduleConfigs, quickUpdate }) {
|
|
7
|
+
this.directory = directory;
|
|
8
|
+
this.modules = Object.freeze([...modules]);
|
|
9
|
+
this.ides = Object.freeze([...ides]);
|
|
10
|
+
this.skipPrompts = skipPrompts;
|
|
11
|
+
this.verbose = verbose;
|
|
12
|
+
this.actionType = actionType;
|
|
13
|
+
this.coreConfig = coreConfig;
|
|
14
|
+
this.moduleConfigs = moduleConfigs;
|
|
15
|
+
this._quickUpdate = quickUpdate;
|
|
16
|
+
Object.freeze(this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a clean install config from raw user input.
|
|
21
|
+
* @param {Object} userInput - UI answers or CLI flags
|
|
22
|
+
* @returns {Config}
|
|
23
|
+
*/
|
|
24
|
+
static build(userInput) {
|
|
25
|
+
const modules = [...(userInput.modules || [])];
|
|
26
|
+
if (userInput.installCore && !modules.includes('core')) {
|
|
27
|
+
modules.unshift('core');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Config({
|
|
31
|
+
directory: userInput.directory,
|
|
32
|
+
modules,
|
|
33
|
+
ides: userInput.skipIde ? [] : [...(userInput.ides || [])],
|
|
34
|
+
skipPrompts: userInput.skipPrompts || false,
|
|
35
|
+
verbose: userInput.verbose || false,
|
|
36
|
+
actionType: userInput.actionType,
|
|
37
|
+
coreConfig: userInput.coreConfig || {},
|
|
38
|
+
moduleConfigs: userInput.moduleConfigs || null,
|
|
39
|
+
quickUpdate: userInput._quickUpdate || false,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hasCoreConfig() {
|
|
44
|
+
return this.coreConfig && Object.keys(this.coreConfig).length > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isQuickUpdate() {
|
|
48
|
+
return this._quickUpdate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { Config };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const yaml = require('yaml');
|
|
4
|
+
const { Manifest } = require('./manifest');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Immutable snapshot of an existing BMAD installation.
|
|
8
|
+
* Pure query object — no filesystem operations after construction.
|
|
9
|
+
*/
|
|
10
|
+
class ExistingInstall {
|
|
11
|
+
#version;
|
|
12
|
+
|
|
13
|
+
constructor({ installed, version, hasCore, modules, ides, customModules }) {
|
|
14
|
+
this.installed = installed;
|
|
15
|
+
this.#version = version;
|
|
16
|
+
this.hasCore = hasCore;
|
|
17
|
+
this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));
|
|
18
|
+
this.moduleIds = Object.freeze(this.modules.map((m) => m.id));
|
|
19
|
+
this.ides = Object.freeze([...ides]);
|
|
20
|
+
this.customModules = Object.freeze([...customModules]);
|
|
21
|
+
Object.freeze(this);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get version() {
|
|
25
|
+
if (!this.installed) {
|
|
26
|
+
throw new Error('version is not available when nothing is installed');
|
|
27
|
+
}
|
|
28
|
+
return this.#version;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static empty() {
|
|
32
|
+
return new ExistingInstall({
|
|
33
|
+
installed: false,
|
|
34
|
+
version: null,
|
|
35
|
+
hasCore: false,
|
|
36
|
+
modules: [],
|
|
37
|
+
ides: [],
|
|
38
|
+
customModules: [],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scan a bmad directory and return an immutable snapshot of what's installed.
|
|
44
|
+
* @param {string} bmadDir - Path to bmad directory
|
|
45
|
+
* @returns {Promise<ExistingInstall>}
|
|
46
|
+
*/
|
|
47
|
+
static async detect(bmadDir) {
|
|
48
|
+
if (!(await fs.pathExists(bmadDir))) {
|
|
49
|
+
return ExistingInstall.empty();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let version = null;
|
|
53
|
+
let hasCore = false;
|
|
54
|
+
const modules = [];
|
|
55
|
+
let ides = [];
|
|
56
|
+
let customModules = [];
|
|
57
|
+
|
|
58
|
+
const manifest = new Manifest();
|
|
59
|
+
const manifestData = await manifest.read(bmadDir);
|
|
60
|
+
if (manifestData) {
|
|
61
|
+
version = manifestData.version;
|
|
62
|
+
if (manifestData.customModules) {
|
|
63
|
+
customModules = manifestData.customModules;
|
|
64
|
+
}
|
|
65
|
+
if (manifestData.ides) {
|
|
66
|
+
ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const corePath = path.join(bmadDir, 'core');
|
|
71
|
+
if (await fs.pathExists(corePath)) {
|
|
72
|
+
hasCore = true;
|
|
73
|
+
|
|
74
|
+
if (!version) {
|
|
75
|
+
const coreConfigPath = path.join(corePath, 'config.yaml');
|
|
76
|
+
if (await fs.pathExists(coreConfigPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const configContent = await fs.readFile(coreConfigPath, 'utf8');
|
|
79
|
+
const config = yaml.parse(configContent);
|
|
80
|
+
if (config.version) {
|
|
81
|
+
version = config.version;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Ignore config read errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (manifestData && manifestData.modules && manifestData.modules.length > 0) {
|
|
91
|
+
for (const moduleId of manifestData.modules) {
|
|
92
|
+
const modulePath = path.join(bmadDir, moduleId);
|
|
93
|
+
const moduleConfigPath = path.join(modulePath, 'config.yaml');
|
|
94
|
+
|
|
95
|
+
const moduleInfo = {
|
|
96
|
+
id: moduleId,
|
|
97
|
+
path: modulePath,
|
|
98
|
+
version: 'unknown',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (await fs.pathExists(moduleConfigPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
|
|
104
|
+
const config = yaml.parse(configContent);
|
|
105
|
+
moduleInfo.version = config.version || 'unknown';
|
|
106
|
+
moduleInfo.name = config.name || moduleId;
|
|
107
|
+
moduleInfo.description = config.description;
|
|
108
|
+
} catch {
|
|
109
|
+
// Ignore config read errors
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
modules.push(moduleInfo);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const installed = hasCore || modules.length > 0 || !!manifestData;
|
|
118
|
+
|
|
119
|
+
if (!installed) {
|
|
120
|
+
return ExistingInstall.empty();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return new ExistingInstall({ installed, version, hasCore, modules, ides, customModules });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { ExistingInstall };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { getProjectRoot } = require('../project-root');
|
|
4
|
+
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|
5
|
+
|
|
6
|
+
class InstallPaths {
|
|
7
|
+
static async create(config) {
|
|
8
|
+
const srcDir = getProjectRoot();
|
|
9
|
+
await assertReadableDir(srcDir, 'BMAD source root');
|
|
10
|
+
|
|
11
|
+
const pkgPath = path.join(srcDir, 'package.json');
|
|
12
|
+
await assertReadableFile(pkgPath, 'package.json');
|
|
13
|
+
const version = require(pkgPath).version;
|
|
14
|
+
|
|
15
|
+
const projectRoot = path.resolve(config.directory);
|
|
16
|
+
await ensureWritableDir(projectRoot, 'project root');
|
|
17
|
+
|
|
18
|
+
const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME);
|
|
19
|
+
const isUpdate = await fs.pathExists(bmadDir);
|
|
20
|
+
|
|
21
|
+
const configDir = path.join(bmadDir, '_config');
|
|
22
|
+
const agentsDir = path.join(configDir, 'agents');
|
|
23
|
+
const customCacheDir = path.join(configDir, 'custom');
|
|
24
|
+
const coreDir = path.join(bmadDir, 'core');
|
|
25
|
+
|
|
26
|
+
for (const [dir, label] of [
|
|
27
|
+
[bmadDir, 'bmad directory'],
|
|
28
|
+
[configDir, 'config directory'],
|
|
29
|
+
[agentsDir, 'agents config directory'],
|
|
30
|
+
[customCacheDir, 'custom modules cache'],
|
|
31
|
+
[coreDir, 'core module directory'],
|
|
32
|
+
]) {
|
|
33
|
+
await ensureWritableDir(dir, label);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new InstallPaths({
|
|
37
|
+
srcDir,
|
|
38
|
+
version,
|
|
39
|
+
projectRoot,
|
|
40
|
+
bmadDir,
|
|
41
|
+
configDir,
|
|
42
|
+
agentsDir,
|
|
43
|
+
customCacheDir,
|
|
44
|
+
coreDir,
|
|
45
|
+
isUpdate,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
constructor(props) {
|
|
50
|
+
Object.assign(this, props);
|
|
51
|
+
Object.freeze(this);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
manifestFile() {
|
|
55
|
+
return path.join(this.configDir, 'manifest.yaml');
|
|
56
|
+
}
|
|
57
|
+
agentManifest() {
|
|
58
|
+
return path.join(this.configDir, 'agent-manifest.csv');
|
|
59
|
+
}
|
|
60
|
+
filesManifest() {
|
|
61
|
+
return path.join(this.configDir, 'files-manifest.csv');
|
|
62
|
+
}
|
|
63
|
+
helpCatalog() {
|
|
64
|
+
return path.join(this.configDir, 'bmad-help.csv');
|
|
65
|
+
}
|
|
66
|
+
moduleDir(name) {
|
|
67
|
+
return path.join(this.bmadDir, name);
|
|
68
|
+
}
|
|
69
|
+
moduleConfig(name) {
|
|
70
|
+
return path.join(this.bmadDir, name, 'config.yaml');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function assertReadableDir(dirPath, label) {
|
|
75
|
+
const stat = await fs.stat(dirPath).catch(() => null);
|
|
76
|
+
if (!stat) {
|
|
77
|
+
throw new Error(`${label} does not exist: ${dirPath}`);
|
|
78
|
+
}
|
|
79
|
+
if (!stat.isDirectory()) {
|
|
80
|
+
throw new Error(`${label} is not a directory: ${dirPath}`);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(dirPath, fs.constants.R_OK);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error(`${label} is not readable: ${dirPath}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function assertReadableFile(filePath, label) {
|
|
90
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
91
|
+
if (!stat) {
|
|
92
|
+
throw new Error(`${label} does not exist: ${filePath}`);
|
|
93
|
+
}
|
|
94
|
+
if (!stat.isFile()) {
|
|
95
|
+
throw new Error(`${label} is not a file: ${filePath}`);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await fs.access(filePath, fs.constants.R_OK);
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error(`${label} is not readable: ${filePath}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function ensureWritableDir(dirPath, label) {
|
|
105
|
+
const stat = await fs.stat(dirPath).catch(() => null);
|
|
106
|
+
if (stat && !stat.isDirectory()) {
|
|
107
|
+
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await fs.ensureDir(dirPath);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === 'EACCES') {
|
|
114
|
+
throw new Error(`${label}: permission denied creating directory: ${dirPath}`);
|
|
115
|
+
}
|
|
116
|
+
if (error.code === 'ENOSPC') {
|
|
117
|
+
throw new Error(`${label}: no space left on device: ${dirPath}`);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`${label}: cannot create directory: ${dirPath} (${error.message})`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await fs.access(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
124
|
+
} catch {
|
|
125
|
+
throw new Error(`${label} is not writable: ${dirPath}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { InstallPaths };
|