bmad-method 6.8.1-next.9 → 6.9.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/.claude-plugin/marketplace.json +9 -3
- package/package.json +10 -4
- package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +2 -2
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +8 -8
- package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +7 -7
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/SKILL.md +8 -8
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/design-directions.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-ux/assets/key-screens.md +4 -4
- package/src/bmm-skills/2-plan-workflows/bmad-ux/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/creative-tools.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/headless.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-ux/references/validate.md +2 -2
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +2 -2
- package/src/bmm-skills/3-solutioning/bmad-architecture/SKILL.md +85 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/assets/spine-template.md +79 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/customize.toml +100 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/references/headless.md +26 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/references/reviewer-gate.md +13 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/lint_spine.py +257 -0
- package/src/bmm-skills/3-solutioning/bmad-architecture/scripts/tests/test_lint_spine.py +270 -0
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +16 -60
- package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +15 -0
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +20 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/checklist.md +2 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +13 -0
- package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +13 -0
- package/src/bmm-skills/module-help.csv +2 -2
- package/src/core-skills/bmad-brainstorming/SKILL.md +8 -10
- package/src/core-skills/bmad-brainstorming/references/converge.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/finalize.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/headless.md +4 -4
- package/src/core-skills/bmad-brainstorming/references/in-chat-techniques.md +1 -1
- package/src/core-skills/bmad-brainstorming/references/mode-autonomous.md +1 -1
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +2 -2
- package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +1 -1
- package/src/core-skills/bmad-forge-idea/SKILL.md +79 -0
- package/src/core-skills/bmad-forge-idea/customize.toml +42 -0
- package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +270 -0
- package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +138 -0
- package/src/core-skills/bmad-party-mode/SKILL.md +39 -56
- package/src/core-skills/bmad-party-mode/customize.toml +175 -0
- package/src/core-skills/bmad-party-mode/references/create-party.md +70 -0
- package/src/core-skills/bmad-party-mode/references/mode-agent-team.md +11 -0
- package/src/core-skills/bmad-party-mode/references/mode-auto.md +13 -0
- package/src/core-skills/bmad-party-mode/references/mode-subagent.md +19 -0
- package/src/core-skills/bmad-party-mode/references/party-memory.md +51 -0
- package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +272 -0
- package/src/core-skills/bmad-party-mode/scripts/tests/test-resolve_party.py +146 -0
- package/src/core-skills/bmad-spec/SKILL.md +5 -3
- package/src/core-skills/bmad-spec/assets/spec-template.md +3 -3
- package/src/core-skills/module-help.csv +1 -0
- package/src/scripts/resolve_config.py +8 -6
- package/src/scripts/resolve_customization.py +8 -6
- package/tools/installer/commands/install.js +3 -0
- package/tools/installer/core/installer.js +3 -0
- package/tools/installer/core/uv-check.js +97 -0
- package/tools/installer/core/wsl-node-check.js +109 -0
- package/tools/installer/ide/platform-codes.yaml +7 -0
- package/tools/installer/install-messages.yaml +4 -0
- package/tools/installer/ui.js +10 -9
- package/evals/bmm-skills/bmad-product-brief/evals.json +0 -237
- package/evals/bmm-skills/bmad-product-brief/files/branfield-memo.md +0 -46
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/addendum.md +0 -40
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/brief.md +0 -56
- package/evals/bmm-skills/bmad-product-brief/files/forkbird-brief/decision-log.md +0 -27
- package/evals/bmm-skills/bmad-product-brief/files/meridian-mobility-report.md +0 -116
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/addendum.md +0 -41
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/brief.md +0 -57
- package/evals/bmm-skills/bmad-product-brief/files/mossridge-brief/decision-log.md +0 -29
- package/evals/bmm-skills/bmad-product-brief/files/pantry-bridge-interviews.md +0 -90
- package/evals/bmm-skills/bmad-product-brief/files/q2-brainstorm.md +0 -101
- package/evals/bmm-skills/bmad-product-brief/triggers.json +0 -18
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/architecture-decision-template.md +0 -12
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/domain-complexity.csv +0 -13
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/data/project-types.csv +0 -7
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01-init.md +0 -153
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-01b-continue.md +0 -173
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-02-context.md +0 -224
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-03-starter.md +0 -329
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-04-decisions.md +0 -318
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-05-patterns.md +0 -359
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-06-structure.md +0 -379
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-07-validation.md +0 -361
- package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +0 -82
- package/src/core-skills/bmad-brainstorming/scripts/memlog.py +0 -202
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_memlog.py +0 -265
- package/tools/installer/core/python-check.js +0 -199
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
# /// script
|
|
2
|
-
# requires-python = ">=3.10"
|
|
3
|
-
# dependencies = ["pytest>=8.0"]
|
|
4
|
-
# ///
|
|
5
|
-
"""Tests for memlog.py. Run: uv run --with pytest pytest scripts/tests/test_memlog.py
|
|
6
|
-
|
|
7
|
-
The spine under test is the flat, append-only, chronological invariant: every entry is
|
|
8
|
-
one line recorded at the end in the order it happened — no sections, no grouping.
|
|
9
|
-
"""
|
|
10
|
-
import json
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
import pytest
|
|
15
|
-
|
|
16
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
17
|
-
import memlog # noqa: E402
|
|
18
|
-
|
|
19
|
-
MEMLOG = ".memlog.md"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@pytest.fixture
|
|
23
|
-
def ws(tmp_path):
|
|
24
|
-
return str(tmp_path)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def read(ws):
|
|
28
|
-
return (Path(ws) / MEMLOG).read_text(encoding="utf-8")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def body_of(ws):
|
|
32
|
-
return memlog.split(read(ws))[1]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def entries(ws):
|
|
36
|
-
return [ln for ln in body_of(ws).splitlines() if ln.startswith("- ")]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def init(ws, **fields):
|
|
40
|
-
fields = fields or {"topic": "Reinvent the lunchbox", "goal": "ideas for a pitch"}
|
|
41
|
-
argv = ["init", "--workspace", ws]
|
|
42
|
-
for k, v in fields.items():
|
|
43
|
-
argv += ["--field", f"{k}={v}"]
|
|
44
|
-
assert memlog.main(argv) == 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def append(ws, text, entry_type=None, by=None):
|
|
48
|
-
argv = ["append", "--workspace", ws, "--text", text]
|
|
49
|
-
if entry_type:
|
|
50
|
-
argv += ["--type", entry_type]
|
|
51
|
-
if by:
|
|
52
|
-
argv += ["--by", by]
|
|
53
|
-
assert memlog.main(argv) == 0
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# --- init ---------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
def test_init_writes_frontmatter_fields(ws):
|
|
59
|
-
init(ws)
|
|
60
|
-
meta, body = memlog.split(read(ws))
|
|
61
|
-
assert meta["topic"] == "Reinvent the lunchbox"
|
|
62
|
-
assert meta["goal"] == "ideas for a pitch"
|
|
63
|
-
assert meta["status"] == "active"
|
|
64
|
-
assert "updated" in meta
|
|
65
|
-
assert body.strip() == ""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_init_arbitrary_fields(ws):
|
|
69
|
-
init(ws, topic="T", audience="board")
|
|
70
|
-
meta, _ = memlog.split(read(ws))
|
|
71
|
-
assert meta["audience"] == "board"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_init_refuses_overwrite(ws):
|
|
75
|
-
init(ws)
|
|
76
|
-
assert memlog.main(["init", "--workspace", ws, "--field", "topic=other"]) == 2
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_init_creates_missing_workspace(tmp_path):
|
|
80
|
-
nested = str(tmp_path / "a" / "b")
|
|
81
|
-
assert memlog.main(["init", "--workspace", nested, "--field", "topic=T"]) == 0
|
|
82
|
-
assert (Path(nested) / MEMLOG).is_file()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_init_rejects_malformed_field(ws):
|
|
86
|
-
assert memlog.main(["init", "--workspace", ws, "--field", "noequals"]) == 2
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# --- append: flat chronological order is the whole point -----------------
|
|
90
|
-
|
|
91
|
-
def test_append_lands_at_end_in_order(ws):
|
|
92
|
-
init(ws)
|
|
93
|
-
append(ws, "first")
|
|
94
|
-
append(ws, "second")
|
|
95
|
-
append(ws, "third")
|
|
96
|
-
assert entries(ws) == ["- first", "- second", "- third"]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_no_sections_or_headings_ever(ws):
|
|
100
|
-
init(ws)
|
|
101
|
-
append(ws, "started foo", entry_type="technique")
|
|
102
|
-
append(ws, "an idea", entry_type="idea")
|
|
103
|
-
append(ws, "started bar", entry_type="technique")
|
|
104
|
-
assert "## " not in body_of(ws) # the flat log never grows headings
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def test_type_renders_as_inline_tag(ws):
|
|
108
|
-
init(ws)
|
|
109
|
-
append(ws, "the earth revolves around the sun", entry_type="idea")
|
|
110
|
-
append(ws, "how do we handle stampede?", entry_type="question")
|
|
111
|
-
body = body_of(ws)
|
|
112
|
-
assert "- (idea) the earth revolves around the sun" in body
|
|
113
|
-
assert "- (question) how do we handle stampede?" in body
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_append_without_type_is_plain_note(ws):
|
|
117
|
-
init(ws)
|
|
118
|
-
append(ws, "bare entry")
|
|
119
|
-
assert entries(ws) == ["- bare entry"]
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def test_append_collapses_newlines_into_one_line(ws):
|
|
123
|
-
init(ws)
|
|
124
|
-
append(ws, "line one\nline two\n spaced out")
|
|
125
|
-
assert entries(ws) == ["- line one line two spaced out"]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_revisited_technique_is_just_a_later_entry(ws):
|
|
129
|
-
# the user's model: switching techniques is an entry, not a section to return to
|
|
130
|
-
init(ws)
|
|
131
|
-
append(ws, "started SCAMPER", entry_type="technique")
|
|
132
|
-
append(ws, "magnetic latch", entry_type="idea")
|
|
133
|
-
append(ws, "started Six Hats", entry_type="technique")
|
|
134
|
-
append(ws, "stale data risk", entry_type="idea")
|
|
135
|
-
append(ws, "started SCAMPER", entry_type="technique") # back to SCAMPER — just appended again
|
|
136
|
-
append(ws, "stackable tiers", entry_type="idea")
|
|
137
|
-
assert entries(ws) == [
|
|
138
|
-
"- (technique) started SCAMPER",
|
|
139
|
-
"- (idea) magnetic latch",
|
|
140
|
-
"- (technique) started Six Hats",
|
|
141
|
-
"- (idea) stale data risk",
|
|
142
|
-
"- (technique) started SCAMPER",
|
|
143
|
-
"- (idea) stackable tiers",
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def test_by_renders_attribution_in_tag(ws):
|
|
148
|
-
# Creative Partner mode must record whose idea each one was
|
|
149
|
-
init(ws)
|
|
150
|
-
append(ws, "magnetic latch lid", entry_type="idea", by="user")
|
|
151
|
-
append(ws, "lid doubles as a plate", entry_type="idea", by="coach")
|
|
152
|
-
body = body_of(ws)
|
|
153
|
-
assert "- (idea by user) magnetic latch lid" in body
|
|
154
|
-
assert "- (idea by coach) lid doubles as a plate" in body
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def test_by_without_type_renders_alone(ws):
|
|
158
|
-
init(ws)
|
|
159
|
-
append(ws, "off-the-cuff thought", by="coach")
|
|
160
|
-
assert entries(ws) == ["- (by coach) off-the-cuff thought"]
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def test_heterogeneous_entry_types_coexist(ws):
|
|
164
|
-
init(ws)
|
|
165
|
-
append(ws, "an idea", entry_type="idea")
|
|
166
|
-
append(ws, "an open question", entry_type="question")
|
|
167
|
-
append(ws, "a decision we made", entry_type="decision")
|
|
168
|
-
append(ws, "user wants mobile-first", entry_type="direction")
|
|
169
|
-
body = body_of(ws)
|
|
170
|
-
for tag in ("(idea)", "(question)", "(decision)", "(direction)"):
|
|
171
|
-
assert tag in body
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# --- set ----------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
def test_set_flips_status(ws):
|
|
177
|
-
init(ws)
|
|
178
|
-
memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
|
|
179
|
-
assert memlog.split(read(ws))[0]["status"] == "complete"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def test_set_preserves_body(ws):
|
|
183
|
-
init(ws)
|
|
184
|
-
append(ws, "keep me", entry_type="idea")
|
|
185
|
-
memlog.main(["set", "--workspace", ws, "--key", "status", "--value", "complete"])
|
|
186
|
-
meta, body = memlog.split(read(ws))
|
|
187
|
-
assert meta["status"] == "complete"
|
|
188
|
-
assert "- (idea) keep me" in body
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def test_set_can_add_new_field(ws):
|
|
192
|
-
init(ws)
|
|
193
|
-
memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
|
|
194
|
-
assert memlog.split(read(ws))[0]["owner"] == "BMad"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def test_updated_stays_last(ws):
|
|
198
|
-
init(ws)
|
|
199
|
-
memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
|
|
200
|
-
meta = memlog.split(read(ws))[0]
|
|
201
|
-
assert list(meta)[-1] == "updated"
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# --- robustness ---------------------------------------------------------
|
|
205
|
-
|
|
206
|
-
def test_roundtrip_render_is_stable(ws):
|
|
207
|
-
init(ws)
|
|
208
|
-
append(ws, "one", entry_type="idea")
|
|
209
|
-
first = read(ws)
|
|
210
|
-
meta, body = memlog.split(first)
|
|
211
|
-
assert memlog.render(meta, body) == first
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def test_commas_in_field_survive(ws):
|
|
215
|
-
init(ws, topic="cars, trains, and planes")
|
|
216
|
-
append(ws, "z", entry_type="idea")
|
|
217
|
-
meta, _ = memlog.split(read(ws))
|
|
218
|
-
assert meta["topic"] == "cars, trains, and planes"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def test_triple_dash_in_field_does_not_corrupt_frontmatter(ws):
|
|
222
|
-
# A `---` inside a value must NOT be read as the closing fence: topic stays intact,
|
|
223
|
-
# status survives, and the body never leaks frontmatter text.
|
|
224
|
-
init(ws, topic="Pricing --- tiers --- and add-ons")
|
|
225
|
-
append(ws, "an idea", entry_type="idea")
|
|
226
|
-
meta, body = memlog.split(read(ws))
|
|
227
|
-
assert meta["topic"] == "Pricing --- tiers --- and add-ons"
|
|
228
|
-
assert meta["status"] == "active"
|
|
229
|
-
assert entries(ws) == ["- (idea) an idea"]
|
|
230
|
-
assert "status:" not in body # frontmatter never bled into the body
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def test_triple_dash_status_survives_in_ack(ws, capsys):
|
|
234
|
-
init(ws, topic="a --- b")
|
|
235
|
-
append(ws, "x", entry_type="idea")
|
|
236
|
-
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
237
|
-
assert out["status"] == "active" # not "" — frontmatter recovered cleanly
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def test_newline_in_field_is_neutralized(ws):
|
|
241
|
-
# A value carrying a newline can't break the fence on the next round-trip.
|
|
242
|
-
memlog.main(["init", "--workspace", ws, "--field", "topic=line one\nline two"])
|
|
243
|
-
append(ws, "x", entry_type="idea")
|
|
244
|
-
meta, _ = memlog.split(read(ws))
|
|
245
|
-
assert "\n" not in meta["topic"]
|
|
246
|
-
assert meta["status"] == "active"
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def test_append_emits_json_ack(ws, capsys):
|
|
250
|
-
init(ws)
|
|
251
|
-
append(ws, "x", entry_type="idea")
|
|
252
|
-
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
253
|
-
assert out["ok"] is True
|
|
254
|
-
assert out["status"] == "active"
|
|
255
|
-
assert out["entries"] == 1
|
|
256
|
-
assert out["memlog"].endswith(MEMLOG)
|
|
257
|
-
assert "section" not in out # sections are gone
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def test_ack_entry_count_climbs(ws, capsys):
|
|
261
|
-
init(ws)
|
|
262
|
-
append(ws, "a")
|
|
263
|
-
append(ws, "b")
|
|
264
|
-
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
265
|
-
assert out["entries"] == 2
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
const { spawnSync } = require('node:child_process');
|
|
2
|
-
const prompts = require('../prompts');
|
|
3
|
-
|
|
4
|
-
// Python 3.11 added stdlib `tomllib` (PEP 680), which the shared scripts in
|
|
5
|
-
// src/scripts/ (resolve_config.py, resolve_customization.py) require to read
|
|
6
|
-
// BMAD's TOML config files. memlog.py is more lenient and runs on 3.8+.
|
|
7
|
-
const PYTHON_FULL_SUPPORT = { major: 3, minor: 11 };
|
|
8
|
-
const PYTHON_PARTIAL_SUPPORT = { major: 3, minor: 8 };
|
|
9
|
-
|
|
10
|
-
// Every runtime call site (skill steps, on_complete hooks) invokes a literal
|
|
11
|
-
// `python3`, so only that command's version vouches for BMAD features. The
|
|
12
|
-
// fallback probes exist to tell the user "Python is installed, but not under
|
|
13
|
-
// the name BMAD uses" instead of a misleading "No Python found".
|
|
14
|
-
const RUNTIME_COMMAND = 'python3';
|
|
15
|
-
const PROBE_CANDIDATES =
|
|
16
|
-
process.platform === 'win32'
|
|
17
|
-
? [
|
|
18
|
-
{ command: 'python3', args: ['--version'] },
|
|
19
|
-
{ command: 'py', args: ['-3', '--version'] },
|
|
20
|
-
{ command: 'python', args: ['--version'] },
|
|
21
|
-
]
|
|
22
|
-
: [
|
|
23
|
-
{ command: 'python3', args: ['--version'] },
|
|
24
|
-
{ command: 'python', args: ['--version'] },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Parse a `python --version` output line into version parts.
|
|
29
|
-
* Python 3 prints to stdout; Python 2 printed to stderr — callers pass both.
|
|
30
|
-
* @param {string} output - Combined stdout/stderr from `python --version`
|
|
31
|
-
* @returns {{major: number, minor: number, patch: number, raw: string}|null}
|
|
32
|
-
*/
|
|
33
|
-
function parsePythonVersion(output) {
|
|
34
|
-
if (!output) return null;
|
|
35
|
-
const match = output.match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
36
|
-
if (!match) return null;
|
|
37
|
-
return {
|
|
38
|
-
major: Number(match[1]),
|
|
39
|
-
minor: Number(match[2]),
|
|
40
|
-
patch: Number(match[3] || 0),
|
|
41
|
-
raw: `${match[1]}.${match[2]}.${match[3] || 0}`,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Classify a detected Python version against BMAD's feature requirements.
|
|
47
|
-
* @param {{major: number, minor: number}|null} version
|
|
48
|
-
* @returns {'full'|'partial'|'unsupported'|'none'}
|
|
49
|
-
*/
|
|
50
|
-
function classifyPython(version) {
|
|
51
|
-
if (!version) return 'none';
|
|
52
|
-
const { major, minor } = version;
|
|
53
|
-
if (major > PYTHON_FULL_SUPPORT.major || (major === PYTHON_FULL_SUPPORT.major && minor >= PYTHON_FULL_SUPPORT.minor)) {
|
|
54
|
-
return 'full';
|
|
55
|
-
}
|
|
56
|
-
if (major === PYTHON_PARTIAL_SUPPORT.major && minor >= PYTHON_PARTIAL_SUPPORT.minor) {
|
|
57
|
-
return 'partial';
|
|
58
|
-
}
|
|
59
|
-
return 'unsupported';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Run one probe candidate and return its parsed version, or null.
|
|
64
|
-
* @param {{command: string, args: string[]}} candidate
|
|
65
|
-
* @returns {{major: number, minor: number, patch: number, raw: string}|null}
|
|
66
|
-
*/
|
|
67
|
-
function probeVersion(candidate) {
|
|
68
|
-
const run = (extra = {}) =>
|
|
69
|
-
spawnSync(candidate.command, candidate.args, {
|
|
70
|
-
encoding: 'utf8',
|
|
71
|
-
timeout: 5000,
|
|
72
|
-
windowsHide: true,
|
|
73
|
-
...extra,
|
|
74
|
-
});
|
|
75
|
-
let result = run();
|
|
76
|
-
// Node >=18.20/20.12 refuses to spawn .bat/.cmd without a shell
|
|
77
|
-
// (CVE-2024-27980 hardening) and reports EINVAL — pyenv-win ships its
|
|
78
|
-
// python shims as .bat. Args here are static literals, so a shell retry
|
|
79
|
-
// is injection-safe.
|
|
80
|
-
if (result.error && result.error.code === 'EINVAL' && process.platform === 'win32') {
|
|
81
|
-
result = run({ shell: true });
|
|
82
|
-
}
|
|
83
|
-
if (result.error) return null;
|
|
84
|
-
return parsePythonVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Probe the local environment for a Python interpreter.
|
|
89
|
-
* Tries each candidate command and returns the first that reports a version.
|
|
90
|
-
* `isRuntimeCommand` is true only when the match is `python3` — the command
|
|
91
|
-
* BMAD scripts actually invoke.
|
|
92
|
-
* @returns {{command: string, version: {major: number, minor: number, patch: number, raw: string}, isRuntimeCommand: boolean}|null}
|
|
93
|
-
*/
|
|
94
|
-
function detectPython() {
|
|
95
|
-
for (const candidate of PROBE_CANDIDATES) {
|
|
96
|
-
try {
|
|
97
|
-
const version = probeVersion(candidate);
|
|
98
|
-
if (version) {
|
|
99
|
-
const display = candidate.args.length > 1 ? `${candidate.command} ${candidate.args.slice(0, -1).join(' ')}` : candidate.command;
|
|
100
|
-
return { command: display, version, isRuntimeCommand: candidate.command === RUNTIME_COMMAND };
|
|
101
|
-
}
|
|
102
|
-
} catch {
|
|
103
|
-
// Candidate not runnable — try the next one.
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function upgradeHints() {
|
|
110
|
-
return [
|
|
111
|
-
'How to get Python 3.11+ (as `python3`):',
|
|
112
|
-
' macOS: brew install python3',
|
|
113
|
-
' Windows: winget install Python.Python.3.12 (then ensure `python3` resolves, e.g. enable the python3 alias)',
|
|
114
|
-
' Linux/WSL: sudo apt install python3 (Ubuntu 24.04+ ships 3.12; older distros: use pyenv or deadsnakes)',
|
|
115
|
-
' Docker: add python3 to your image (e.g. apk add python3 / apt-get install -y python3)',
|
|
116
|
-
].join('\n');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Check the local Python environment and warn about degraded BMAD features.
|
|
121
|
-
*
|
|
122
|
-
* Warn-don't-block: most of BMAD works without Python, so the install always
|
|
123
|
-
* may proceed — but the user must explicitly acknowledge the warning so it
|
|
124
|
-
* can't scroll past unseen. In non-interactive runs (--yes, or stdin is not
|
|
125
|
-
* a TTY) the warning is logged and the install continues without a prompt.
|
|
126
|
-
*
|
|
127
|
-
* @param {Object} [options]
|
|
128
|
-
* @param {boolean} [options.nonInteractive=false] - Skip the ack prompt (--yes, or no TTY)
|
|
129
|
-
* @returns {Promise<{status: string, detected: Object|null}>}
|
|
130
|
-
*/
|
|
131
|
-
async function checkPythonEnvironment({ nonInteractive = false } = {}) {
|
|
132
|
-
// Called via module.exports so tests can stub detection.
|
|
133
|
-
const detected = module.exports.detectPython();
|
|
134
|
-
const status = classifyPython(detected ? detected.version : null);
|
|
135
|
-
|
|
136
|
-
if (status === 'full' && detected.isRuntimeCommand) {
|
|
137
|
-
await prompts.log.success(`Python ${detected.version.raw} detected (${detected.command}) — all BMAD features supported.`);
|
|
138
|
-
return { status, detected };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (detected && !detected.isRuntimeCommand) {
|
|
142
|
-
await prompts.log.warn(
|
|
143
|
-
`Python ${detected.version.raw} found via \`${detected.command}\`, but BMAD scripts invoke \`python3\`, which is not on PATH.\n` +
|
|
144
|
-
`Python-powered features (memlog session memory, TOML config resolution) won't run until \`python3\` resolves —\n` +
|
|
145
|
-
`add a python3 alias/shim, or reinstall Python with the python3 launcher enabled.`,
|
|
146
|
-
);
|
|
147
|
-
} else if (status === 'partial') {
|
|
148
|
-
await prompts.log.warn(
|
|
149
|
-
`Python ${detected.version.raw} detected (${detected.command}) — BMAD's TOML config tools need Python 3.11+ (stdlib tomllib).\n` +
|
|
150
|
-
`Works: memlog session memory. Won't work: config/customization resolution scripts.`,
|
|
151
|
-
);
|
|
152
|
-
} else {
|
|
153
|
-
const found =
|
|
154
|
-
status === 'unsupported' ? `Python ${detected.version.raw} detected (${detected.command}) — too old.` : 'No Python found on PATH.';
|
|
155
|
-
await prompts.log.warn(
|
|
156
|
-
`${found} BMAD installs fine without it, but Python-powered features\n` +
|
|
157
|
-
`(memlog session memory, TOML config resolution) won't run until Python 3.11+ is available.`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
await prompts.note(upgradeHints(), 'Python 3.11+ recommended');
|
|
161
|
-
|
|
162
|
-
if (nonInteractive) {
|
|
163
|
-
await prompts.log.info('Continuing anyway (non-interactive run). You can fix Python later — no reinstall needed.');
|
|
164
|
-
return { status, detected };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const choice = await prompts.select({
|
|
168
|
-
message: "BMAD's Python-powered features won't work yet. How do you want to proceed?",
|
|
169
|
-
choices: [
|
|
170
|
-
{
|
|
171
|
-
name: 'Continue install',
|
|
172
|
-
value: 'continue',
|
|
173
|
-
hint: 'BMAD works without Python — you can fix Python later, no reinstall needed',
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: 'Quit and fix Python first',
|
|
177
|
-
value: 'quit',
|
|
178
|
-
hint: 'make Python 3.11+ available as python3, then re-run the installer',
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
default: 'continue',
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
if (choice === 'quit') {
|
|
185
|
-
await prompts.cancel('Make Python 3.11+ available as `python3` (see hints above), then re-run the installer.');
|
|
186
|
-
process.exit(0);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return { status, detected };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
module.exports = {
|
|
193
|
-
checkPythonEnvironment,
|
|
194
|
-
detectPython,
|
|
195
|
-
parsePythonVersion,
|
|
196
|
-
classifyPython,
|
|
197
|
-
PYTHON_FULL_SUPPORT,
|
|
198
|
-
PYTHON_PARTIAL_SUPPORT,
|
|
199
|
-
};
|