bmad-method 6.8.1-next.2 → 6.8.1-next.20
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/3-solutioning/bmad-create-epics-and-stories/steps/step-01-validate-prerequisites.md +12 -4
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +29 -14
- package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +1 -1
- 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 +25 -9
- package/src/core-skills/bmad-spec/assets/headless-schemas.md +3 -3
- package/src/core-skills/bmad-spec/assets/spec-template.md +4 -4
- package/src/core-skills/module-help.csv +1 -0
- package/src/{core-skills/bmad-brainstorming/scripts → scripts}/memlog.py +56 -34
- package/src/scripts/resolve_config.py +8 -6
- package/src/scripts/resolve_customization.py +8 -6
- package/src/{core-skills/bmad-brainstorming/scripts → scripts}/tests/test_memlog.py +68 -27
- package/tools/installer/commands/install.js +3 -0
- package/tools/installer/core/installer.js +35 -1
- 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 +14 -0
- package/tools/installer/install-messages.yaml +4 -0
- package/tools/installer/ui.js +11 -0
- 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
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"""Tests for memlog.py. Run: uv run --with pytest pytest scripts/tests/test_memlog.py
|
|
6
6
|
|
|
7
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
|
|
8
|
+
one line recorded at the end in the order it happened — no sections, no grouping, and no
|
|
9
|
+
lifecycle status the log would have to mutate.
|
|
9
10
|
"""
|
|
10
11
|
import json
|
|
11
12
|
import sys
|
|
@@ -60,11 +61,17 @@ def test_init_writes_frontmatter_fields(ws):
|
|
|
60
61
|
meta, body = memlog.split(read(ws))
|
|
61
62
|
assert meta["topic"] == "Reinvent the lunchbox"
|
|
62
63
|
assert meta["goal"] == "ideas for a pitch"
|
|
63
|
-
assert meta["status"] == "active"
|
|
64
64
|
assert "updated" in meta
|
|
65
65
|
assert body.strip() == ""
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
def test_init_has_no_lifecycle_status(ws):
|
|
69
|
+
# A memory log carries no "status" flag; completion is an appended entry, not frontmatter.
|
|
70
|
+
init(ws)
|
|
71
|
+
meta, _ = memlog.split(read(ws))
|
|
72
|
+
assert "status" not in meta
|
|
73
|
+
|
|
74
|
+
|
|
68
75
|
def test_init_arbitrary_fields(ws):
|
|
69
76
|
init(ws, topic="T", audience="board")
|
|
70
77
|
meta, _ = memlog.split(read(ws))
|
|
@@ -86,6 +93,30 @@ def test_init_rejects_malformed_field(ws):
|
|
|
86
93
|
assert memlog.main(["init", "--workspace", ws, "--field", "noequals"]) == 2
|
|
87
94
|
|
|
88
95
|
|
|
96
|
+
# --- addressing: --workspace and --path are interchangeable --------------
|
|
97
|
+
|
|
98
|
+
def test_path_addressing_targets_the_file_directly(tmp_path):
|
|
99
|
+
target = tmp_path / "run" / ".memlog.md"
|
|
100
|
+
assert memlog.main(["init", "--path", str(target), "--field", "topic=T"]) == 0
|
|
101
|
+
assert target.is_file()
|
|
102
|
+
assert memlog.main(["append", "--path", str(target), "--text", "an idea", "--type", "idea"]) == 0
|
|
103
|
+
body = memlog.split(target.read_text(encoding="utf-8"))[1]
|
|
104
|
+
assert "- (idea) an idea" in body
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_workspace_and_path_resolve_to_same_file(ws):
|
|
108
|
+
init(ws)
|
|
109
|
+
via_path = str(Path(ws) / MEMLOG)
|
|
110
|
+
assert memlog.main(["append", "--path", via_path, "--text", "from path"]) == 0
|
|
111
|
+
assert memlog.main(["append", "--workspace", ws, "--text", "from workspace"]) == 0
|
|
112
|
+
assert entries(ws) == ["- from path", "- from workspace"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_target_is_required(ws):
|
|
116
|
+
with pytest.raises(SystemExit):
|
|
117
|
+
memlog.main(["append", "--text", "orphan"]) # neither --workspace nor --path
|
|
118
|
+
|
|
119
|
+
|
|
89
120
|
# --- append: flat chronological order is the whole point -----------------
|
|
90
121
|
|
|
91
122
|
def test_append_lands_at_end_in_order(ws):
|
|
@@ -119,6 +150,15 @@ def test_append_without_type_is_plain_note(ws):
|
|
|
119
150
|
assert entries(ws) == ["- bare entry"]
|
|
120
151
|
|
|
121
152
|
|
|
153
|
+
def test_completion_is_an_entry_not_a_status(ws):
|
|
154
|
+
# The documented way to mark a session done: append it. Frontmatter never gains a status.
|
|
155
|
+
init(ws)
|
|
156
|
+
append(ws, "session complete", entry_type="event")
|
|
157
|
+
meta, _ = memlog.split(read(ws))
|
|
158
|
+
assert "status" not in meta
|
|
159
|
+
assert entries(ws)[-1] == "- (event) session complete"
|
|
160
|
+
|
|
161
|
+
|
|
122
162
|
def test_append_collapses_newlines_into_one_line(ws):
|
|
123
163
|
init(ws)
|
|
124
164
|
append(ws, "line one\nline two\n spaced out")
|
|
@@ -171,29 +211,39 @@ def test_heterogeneous_entry_types_coexist(ws):
|
|
|
171
211
|
assert tag in body
|
|
172
212
|
|
|
173
213
|
|
|
174
|
-
|
|
214
|
+
def test_free_vocabulary_is_not_enforced(ws):
|
|
215
|
+
# The tool is neutral: any --type the host skill names renders verbatim.
|
|
216
|
+
init(ws)
|
|
217
|
+
append(ws, "a custom kind", entry_type="crack")
|
|
218
|
+
append(ws, "another", entry_type="lock")
|
|
219
|
+
body = body_of(ws)
|
|
220
|
+
assert "- (crack) a custom kind" in body
|
|
221
|
+
assert "- (lock) another" in body
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# --- set: generic descriptive frontmatter, no lifecycle semantics --------
|
|
175
225
|
|
|
176
|
-
def
|
|
226
|
+
def test_set_adds_field(ws):
|
|
177
227
|
init(ws)
|
|
178
|
-
memlog.main(["set", "--workspace", ws, "--key", "
|
|
179
|
-
assert memlog.split(read(ws))[0]["
|
|
228
|
+
memlog.main(["set", "--workspace", ws, "--key", "mode", "--value", "partner"])
|
|
229
|
+
assert memlog.split(read(ws))[0]["mode"] == "partner"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_set_replaces_field(ws):
|
|
233
|
+
init(ws, topic="T", mode="facilitator")
|
|
234
|
+
memlog.main(["set", "--workspace", ws, "--key", "mode", "--value", "partner"])
|
|
235
|
+
assert memlog.split(read(ws))[0]["mode"] == "partner"
|
|
180
236
|
|
|
181
237
|
|
|
182
238
|
def test_set_preserves_body(ws):
|
|
183
239
|
init(ws)
|
|
184
240
|
append(ws, "keep me", entry_type="idea")
|
|
185
|
-
memlog.main(["set", "--workspace", ws, "--key", "
|
|
241
|
+
memlog.main(["set", "--workspace", ws, "--key", "mode", "--value", "partner"])
|
|
186
242
|
meta, body = memlog.split(read(ws))
|
|
187
|
-
assert meta["
|
|
243
|
+
assert meta["mode"] == "partner"
|
|
188
244
|
assert "- (idea) keep me" in body
|
|
189
245
|
|
|
190
246
|
|
|
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
247
|
def test_updated_stays_last(ws):
|
|
198
248
|
init(ws)
|
|
199
249
|
memlog.main(["set", "--workspace", ws, "--key", "owner", "--value", "BMad"])
|
|
@@ -219,22 +269,14 @@ def test_commas_in_field_survive(ws):
|
|
|
219
269
|
|
|
220
270
|
|
|
221
271
|
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
|
-
#
|
|
272
|
+
# A `---` inside a value must NOT be read as the closing fence: topic stays intact
|
|
273
|
+
# and the body never leaks frontmatter text.
|
|
224
274
|
init(ws, topic="Pricing --- tiers --- and add-ons")
|
|
225
275
|
append(ws, "an idea", entry_type="idea")
|
|
226
276
|
meta, body = memlog.split(read(ws))
|
|
227
277
|
assert meta["topic"] == "Pricing --- tiers --- and add-ons"
|
|
228
|
-
assert meta["status"] == "active"
|
|
229
278
|
assert entries(ws) == ["- (idea) an idea"]
|
|
230
|
-
assert "
|
|
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
|
|
279
|
+
assert "topic:" not in body # frontmatter never bled into the body
|
|
238
280
|
|
|
239
281
|
|
|
240
282
|
def test_newline_in_field_is_neutralized(ws):
|
|
@@ -243,7 +285,6 @@ def test_newline_in_field_is_neutralized(ws):
|
|
|
243
285
|
append(ws, "x", entry_type="idea")
|
|
244
286
|
meta, _ = memlog.split(read(ws))
|
|
245
287
|
assert "\n" not in meta["topic"]
|
|
246
|
-
assert meta["status"] == "active"
|
|
247
288
|
|
|
248
289
|
|
|
249
290
|
def test_append_emits_json_ack(ws, capsys):
|
|
@@ -251,9 +292,9 @@ def test_append_emits_json_ack(ws, capsys):
|
|
|
251
292
|
append(ws, "x", entry_type="idea")
|
|
252
293
|
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
253
294
|
assert out["ok"] is True
|
|
254
|
-
assert out["status"] == "active"
|
|
255
295
|
assert out["entries"] == 1
|
|
256
296
|
assert out["memlog"].endswith(MEMLOG)
|
|
297
|
+
assert "status" not in out # no lifecycle status
|
|
257
298
|
assert "section" not in out # sections are gone
|
|
258
299
|
|
|
259
300
|
|
|
@@ -75,6 +75,9 @@ module.exports = {
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const { checkWindowsNodeFromWsl } = require('../core/wsl-node-check');
|
|
79
|
+
await checkWindowsNodeFromWsl();
|
|
80
|
+
|
|
78
81
|
// Set debug flag as environment variable for all components
|
|
79
82
|
if (options.debug) {
|
|
80
83
|
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
|
@@ -419,10 +419,35 @@ class Installer {
|
|
|
419
419
|
const sourceDir = path.dirname(path.join(bmadDir, relativePath));
|
|
420
420
|
if (await fs.pathExists(sourceDir)) {
|
|
421
421
|
await fs.remove(sourceDir);
|
|
422
|
+
await this._removeEmptyParents(path.dirname(sourceDir), bmadDir);
|
|
422
423
|
}
|
|
423
424
|
}
|
|
424
425
|
}
|
|
425
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Remove now-empty parent directories left behind after skill dir cleanup.
|
|
429
|
+
* Walks up from dir, stopping at (and never removing) bmadDir. Best-effort:
|
|
430
|
+
* a directory that vanishes or fills in mid-walk just ends the walk.
|
|
431
|
+
* @param {string} dir - Directory to start walking up from
|
|
432
|
+
* @param {string} bmadDir - BMAD installation directory (boundary)
|
|
433
|
+
*/
|
|
434
|
+
async _removeEmptyParents(dir, bmadDir) {
|
|
435
|
+
let current = dir;
|
|
436
|
+
while (true) {
|
|
437
|
+
// Path-boundary check (not a string prefix, so siblings like _bmad2 don't match).
|
|
438
|
+
const rel = path.relative(bmadDir, current);
|
|
439
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) break;
|
|
440
|
+
try {
|
|
441
|
+
const entries = await fs.readdir(current);
|
|
442
|
+
if (entries.length > 0) break;
|
|
443
|
+
await fs.rmdir(current);
|
|
444
|
+
} catch {
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
current = path.dirname(current);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
426
451
|
async _readSkillManifestRows(bmadDir) {
|
|
427
452
|
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
|
428
453
|
if (!(await fs.pathExists(csvPath))) return [];
|
|
@@ -630,6 +655,7 @@ class Installer {
|
|
|
630
655
|
/**
|
|
631
656
|
* Sync src/scripts/* → _bmad/scripts/ so shared Python scripts
|
|
632
657
|
* (e.g. resolve_customization.py) are available at install time.
|
|
658
|
+
* Excludes dev-only tests and Python caches so they don't ship to users.
|
|
633
659
|
* Wipes the destination first so files removed or renamed in source
|
|
634
660
|
* don't linger and get recorded as installed. Also seeds
|
|
635
661
|
* _bmad/custom/.gitignore on fresh installs so *.user.toml overrides
|
|
@@ -643,7 +669,12 @@ class Installer {
|
|
|
643
669
|
|
|
644
670
|
await fs.remove(paths.scriptsDir);
|
|
645
671
|
await fs.ensureDir(paths.scriptsDir);
|
|
646
|
-
|
|
672
|
+
// Ship only the runtime scripts — dev-only tests and Python caches must not land in user projects.
|
|
673
|
+
const isInstallable = (srcPath) => {
|
|
674
|
+
const base = path.basename(srcPath);
|
|
675
|
+
return base !== 'tests' && base !== '__pycache__' && base !== '.pytest_cache' && !base.endsWith('.pyc');
|
|
676
|
+
};
|
|
677
|
+
await fs.copy(srcScriptsDir, paths.scriptsDir, { overwrite: true, filter: isInstallable });
|
|
647
678
|
await this._trackFilesRecursive(paths.scriptsDir);
|
|
648
679
|
|
|
649
680
|
const customGitignore = path.join(paths.customDir, '.gitignore');
|
|
@@ -1202,6 +1233,9 @@ class Installer {
|
|
|
1202
1233
|
` 1. Launch your AI agent from your project folder`,
|
|
1203
1234
|
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
|
|
1204
1235
|
'',
|
|
1236
|
+
` ${color.cyan('Tip:')} BMAD workflows increasingly run Python scripts via ${color.cyan('uv run')} — uv is`,
|
|
1237
|
+
` becoming the de facto standard. If you don't have it yet, ask your agent to set it up.`,
|
|
1238
|
+
'',
|
|
1205
1239
|
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
|
|
1206
1240
|
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
|
|
1207
1241
|
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { spawnSync } = require('node:child_process');
|
|
2
|
+
const prompts = require('../prompts');
|
|
3
|
+
|
|
4
|
+
// `uv` (https://docs.astral.sh/uv/) is becoming the de facto standard for
|
|
5
|
+
// running the Python scripts BMAD workflows shell out to: `uv run <script>`
|
|
6
|
+
// resolves the interpreter and any dependencies on demand, so skills don't
|
|
7
|
+
// have to assume a particular `python3` is on PATH. The ecosystem is mid-
|
|
8
|
+
// migration — some skills still call `python3` directly — so a missing `uv`
|
|
9
|
+
// is a warning, not a blocker: BMAD installs and runs either way.
|
|
10
|
+
const RUNTIME_COMMAND = 'uv';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse `uv --version` output into version parts.
|
|
14
|
+
* Example outputs: "uv 0.5.31", "uv 0.5.31 (Homebrew 2025-02-12)".
|
|
15
|
+
* @param {string} output - stdout/stderr from `uv --version`
|
|
16
|
+
* @returns {{major: number, minor: number, patch: number, raw: string}|null}
|
|
17
|
+
*/
|
|
18
|
+
function parseUvVersion(output) {
|
|
19
|
+
if (!output) return null;
|
|
20
|
+
const match = output.match(/uv\s+(\d+)\.(\d+)(?:\.(\d+))?/i);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
return {
|
|
23
|
+
major: Number(match[1]),
|
|
24
|
+
minor: Number(match[2]),
|
|
25
|
+
patch: Number(match[3] || 0),
|
|
26
|
+
raw: `${match[1]}.${match[2]}.${match[3] || 0}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Probe the local environment for `uv`.
|
|
32
|
+
* @returns {{version: {major: number, minor: number, patch: number, raw: string}}|null}
|
|
33
|
+
*/
|
|
34
|
+
function detectUv() {
|
|
35
|
+
let result;
|
|
36
|
+
try {
|
|
37
|
+
result = spawnSync(RUNTIME_COMMAND, ['--version'], {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
timeout: 5000,
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (!result || result.error) return null;
|
|
46
|
+
const version = parseUvVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
|
|
47
|
+
return version ? { version } : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setupHints() {
|
|
51
|
+
return [
|
|
52
|
+
'BMAD workflows increasingly run Python scripts via `uv run`, which manages',
|
|
53
|
+
'the interpreter and dependencies for you — no manual venv or pip needed.',
|
|
54
|
+
'',
|
|
55
|
+
'Easiest path: ask your AI agent to "install and set up uv for me".',
|
|
56
|
+
'',
|
|
57
|
+
'Or install it yourself:',
|
|
58
|
+
' macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh',
|
|
59
|
+
' Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"',
|
|
60
|
+
' Homebrew: brew install uv',
|
|
61
|
+
' Docs: https://docs.astral.sh/uv/getting-started/installation/',
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check whether `uv` is available and inform the user.
|
|
67
|
+
*
|
|
68
|
+
* Warn-don't-block, and no acknowledgement prompt: `uv` is on its way to being
|
|
69
|
+
* the standard runner for BMAD's Python scripts, but the migration is still in
|
|
70
|
+
* progress, so the install never stops on its account. The note tells the user
|
|
71
|
+
* how to set it up (preferably by asking their agent).
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<{status: 'found'|'missing', detected: Object|null}>}
|
|
74
|
+
*/
|
|
75
|
+
async function checkUvEnvironment() {
|
|
76
|
+
// Called via module.exports so tests can stub detection.
|
|
77
|
+
const detected = module.exports.detectUv();
|
|
78
|
+
|
|
79
|
+
if (detected) {
|
|
80
|
+
await prompts.log.success(`uv ${detected.version.raw} detected — ready to run BMAD's Python-powered scripts via \`uv run\`.`);
|
|
81
|
+
return { status: 'found', detected };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await prompts.log.warn(
|
|
85
|
+
"uv not found on PATH. uv is becoming the de facto standard for running BMAD's Python\n" +
|
|
86
|
+
'scripts (`uv run <script>`), and it provisions the interpreter for you. BMAD installs\n' +
|
|
87
|
+
'fine without it, but setting up uv now keeps you ahead as workflows adopt it.',
|
|
88
|
+
);
|
|
89
|
+
await prompts.note(setupHints(), 'uv recommended');
|
|
90
|
+
return { status: 'missing', detected: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
checkUvEnvironment,
|
|
95
|
+
detectUv,
|
|
96
|
+
parseUvVersion,
|
|
97
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const prompts = require('../prompts');
|
|
2
|
+
|
|
3
|
+
const WSL_UNC_PATTERN = /^\\\\wsl(?:\.localhost|\$)?\\/i;
|
|
4
|
+
|
|
5
|
+
function normalizePath(value) {
|
|
6
|
+
return typeof value === 'string' ? value.replaceAll('/', '\\').toLowerCase() : '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isLinuxStylePath(value) {
|
|
10
|
+
return (
|
|
11
|
+
typeof value === 'string' &&
|
|
12
|
+
value.startsWith('/') &&
|
|
13
|
+
!value.startsWith('//') &&
|
|
14
|
+
!/^\/[a-z](?:\/|$)/i.test(value) &&
|
|
15
|
+
!/^\/cygdrive\/[a-z](?:\/|$)/i.test(value)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isWslUncPath(value) {
|
|
20
|
+
return WSL_UNC_PATTERN.test(value || '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect the broken interop case where WSL resolved node/npx to Windows.
|
|
25
|
+
* @param {Object} [runtime]
|
|
26
|
+
* @param {string} [runtime.platform]
|
|
27
|
+
* @param {Object} [runtime.env]
|
|
28
|
+
* @param {string} [runtime.cwd]
|
|
29
|
+
* @param {string} [runtime.execPath]
|
|
30
|
+
* @returns {{isMismatch: boolean, reason: string|null, execPath: string}}
|
|
31
|
+
*/
|
|
32
|
+
function detectWindowsNodeFromWsl(runtime = {}) {
|
|
33
|
+
const platform = runtime.platform || process.platform;
|
|
34
|
+
const env = runtime.env || process.env;
|
|
35
|
+
const cwd = runtime.cwd || safeCwd();
|
|
36
|
+
const execPath = runtime.execPath || process.execPath || '';
|
|
37
|
+
|
|
38
|
+
if (platform !== 'win32') {
|
|
39
|
+
return { isMismatch: false, reason: null, execPath };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (env.WSL_DISTRO_NAME) {
|
|
43
|
+
return { isMismatch: true, reason: 'WSL_DISTRO_NAME is set', execPath };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (env.WSL_INTEROP) {
|
|
47
|
+
return { isMismatch: true, reason: 'WSL_INTEROP is set', execPath };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isLinuxStylePath(env.PWD)) {
|
|
51
|
+
return { isMismatch: true, reason: 'PWD is a Linux path', execPath };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isWslUncPath(cwd)) {
|
|
55
|
+
return { isMismatch: true, reason: 'current directory is a WSL UNC path', execPath };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalizedExecPath = normalizePath(execPath);
|
|
59
|
+
if (normalizedExecPath.includes('\\wsl$\\') || normalizedExecPath.includes('\\wsl.localhost\\')) {
|
|
60
|
+
return { isMismatch: true, reason: 'Node executable path is under a WSL UNC path', execPath };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { isMismatch: false, reason: null, execPath };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeCwd() {
|
|
67
|
+
try {
|
|
68
|
+
return process.cwd();
|
|
69
|
+
} catch {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatWindowsNodeFromWslMessage(detection) {
|
|
75
|
+
const lines = [
|
|
76
|
+
'Windows Node.js was launched from a WSL shell.',
|
|
77
|
+
'',
|
|
78
|
+
'This usually means Node.js is not installed inside the WSL distro, so WSL resolved `node`/`npx` to Windows.',
|
|
79
|
+
'The installer cannot safely continue because Linux paths may be interpreted as Windows paths.',
|
|
80
|
+
'',
|
|
81
|
+
'Install Node.js inside WSL, then rerun the same command from the WSL terminal.',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
if (detection.execPath) {
|
|
85
|
+
lines.push('', `Detected Node executable: ${detection.execPath}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (detection.reason) {
|
|
89
|
+
lines.push(`Detection signal: ${detection.reason}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function checkWindowsNodeFromWsl() {
|
|
96
|
+
const detection = module.exports.detectWindowsNodeFromWsl();
|
|
97
|
+
if (!detection.isMismatch) {
|
|
98
|
+
return detection;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await prompts.log.error(formatWindowsNodeFromWslMessage(detection));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
checkWindowsNodeFromWsl,
|
|
107
|
+
detectWindowsNodeFromWsl,
|
|
108
|
+
formatWindowsNodeFromWslMessage,
|
|
109
|
+
};
|
|
@@ -70,6 +70,13 @@ platforms:
|
|
|
70
70
|
target_dir: .agents/skills
|
|
71
71
|
global_target_dir: ~/.codex/skills
|
|
72
72
|
|
|
73
|
+
codewhale:
|
|
74
|
+
name: "CodeWhale"
|
|
75
|
+
preferred: false
|
|
76
|
+
installer:
|
|
77
|
+
target_dir: .codewhale/skills
|
|
78
|
+
global_target_dir: ~/.codewhale/skills
|
|
79
|
+
|
|
73
80
|
codebuddy:
|
|
74
81
|
name: "CodeBuddy"
|
|
75
82
|
preferred: false
|
|
@@ -155,6 +162,13 @@ platforms:
|
|
|
155
162
|
target_dir: .agents/skills
|
|
156
163
|
global_target_dir: ~/.config/agents/skills
|
|
157
164
|
|
|
165
|
+
hermes:
|
|
166
|
+
name: "Hermes Agent"
|
|
167
|
+
preferred: false
|
|
168
|
+
installer:
|
|
169
|
+
target_dir: .agents/skills
|
|
170
|
+
global_target_dir: ~/.hermes/skills
|
|
171
|
+
|
|
158
172
|
iflow:
|
|
159
173
|
name: "iFlow"
|
|
160
174
|
preferred: false
|
|
@@ -12,6 +12,10 @@ startMessage: |
|
|
|
12
12
|
🌟 100% free. 100% open source. Always.
|
|
13
13
|
No paywalls. No gated content. Knowledge shared, not sold.
|
|
14
14
|
|
|
15
|
+
🐍 HEADS UP: uv (https://docs.astral.sh/uv/) is becoming the de facto standard
|
|
16
|
+
for running the Python scripts BMAD workflows rely on (`uv run <script>`).
|
|
17
|
+
If it's not set up yet, ask your AI agent to "install and set up uv for me".
|
|
18
|
+
|
|
15
19
|
🌐 CONNECT:
|
|
16
20
|
Website: https://bmadcode.com/
|
|
17
21
|
Discord: https://discord.gg/gk8jAdXWmj
|
package/tools/installer/ui.js
CHANGED
|
@@ -161,6 +161,17 @@ class UI {
|
|
|
161
161
|
const messageLoader = new MessageLoader();
|
|
162
162
|
await messageLoader.displayStartMessage();
|
|
163
163
|
|
|
164
|
+
// Probe for `uv` before any other prompts: it's becoming the de facto
|
|
165
|
+
// runner for the Python scripts BMAD workflows shell out to
|
|
166
|
+
// (`uv run <script>`), and uv provisions the interpreter itself, so it's
|
|
167
|
+
// the single thing worth checking for. The migration is still in progress
|
|
168
|
+
// (some skills still call `python3` directly), so this is informational —
|
|
169
|
+
// warn-don't-block, no ack prompt — and just points the user at setup
|
|
170
|
+
// (ideally "ask your agent to set up uv"). The installer runs in the
|
|
171
|
+
// destination environment, so probing PATH here tests the right machine.
|
|
172
|
+
const { checkUvEnvironment } = require('./core/uv-check');
|
|
173
|
+
await checkUvEnvironment();
|
|
174
|
+
|
|
164
175
|
// Parse channel flags (--channel/--all-*/--next=/--pin) once. Warnings
|
|
165
176
|
// are surfaced immediately so the user sees them before any git ops run.
|
|
166
177
|
const channelOptions = parseChannelOptions(options);
|