feat(installer): check for uv and standardize messaging on uv run (#2495)
Replace the standalone python3 environment check with a uv check, since uv is becoming the de facto standard for running BMad's Python scripts (`uv run <script>`) and uv provisions the interpreter itself. Installer: - Remove tools/installer/core/python-check.js and its wiring in ui.js - Add tools/installer/core/uv-check.js: warn-don't-block, no ack prompt (the migration is in progress, so a missing uv never blocks install). Missing uv warns and points the user at setup, preferring "ask your agent to set up uv" - Add a uv heads-up to the install intro (install-messages.yaml) and a uv tip line to the final "BMAD is ready" summary box - Swap test Suite 46 from python-check to uv-check coverage Docs and script comments (no functional skill invocations changed): - resolve_config.py / resolve_customization.py docstrings: drop the "No uv ... plain python3 is sufficient" claims; frame uv run as the standard, python3 as the transition fallback; examples use uv run - customize-bmad.md (en/fr/vi-vn): same reframing; example commands use uv run - Update uv run hints in brain.py / list_customizable test comments
This commit is contained in:
parent
005ef1104a
commit
02739932bc
|
|
@ -22,7 +22,7 @@ Le skill `bmad-customize` est un assistant de rédaction guidée pour les **opti
|
|||
:::note[Prérequis]
|
||||
|
||||
- BMad installé dans votre projet (voir [Comment installer BMad](./install-bmad.md))
|
||||
- Python 3.11+ sur votre PATH (pour le script de résolution — utilise `tomllib` de la bibliothèque standard, pas de `pip install`, pas de `uv`, pas de virtualenv)
|
||||
- Un moyen d’exécuter le script de résolution — BMad adopte `uv` comme standard (`uv run`, qui provisionne Python pour vous) ; un simple `python3` 3.11+ sur votre PATH fonctionne toujours pendant la transition. Le script n’utilise que `tomllib` de la bibliothèque standard, il n’y a donc rien à `pip install`.
|
||||
- Un éditeur de texte pour les fichiers TOML
|
||||
:::
|
||||
|
||||
|
|
@ -201,15 +201,15 @@ persistent_facts = [
|
|||
|
||||
## Comment fonctionne la résolution
|
||||
|
||||
À l’activation, le SKILL.md de l’agent exécute un script Python partagé qui effectue la fusion à trois couches et renvoie le bloc résolu en JSON. Le script utilise le module `tomllib` de la bibliothèque standard Python (aucune dépendance externe), donc `python3` suffit :
|
||||
À l’activation, le SKILL.md de l’agent exécute un script Python partagé qui effectue la fusion à trois couches et renvoie le bloc résolu en JSON. Le script utilise uniquement le module `tomllib` de la bibliothèque standard Python (aucune dépendance externe). BMad adopte `uv run` comme standard pour exécuter ces scripts (uv provisionne un Python adapté pour vous) ; un simple `python3` fonctionne toujours pendant la transition :
|
||||
|
||||
```bash
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill {skill-root} \
|
||||
--key agent
|
||||
```
|
||||
|
||||
**Prérequis** : Python 3.11+ (les versions antérieures n’incluent pas `tomllib`). Pas de `pip install`, pas de `uv`, pas de virtualenv. Vérifiez avec `python3 --version`. Certaines plateformes (macOS sans Homebrew, Ubuntu 22.04) ont `python3` par défaut en 3.10 ou antérieur, vous devrez peut-être installer 3.11+ séparément.
|
||||
**Prérequis** : Python 3.11+ (les versions antérieures n’incluent pas `tomllib`). Rien à `pip install`. L’exécution via `uv run` est le standard à venir — uv résout un interpréteur adapté pour vous. Si vous l’exécutez directement avec `python3` pendant la transition, vérifiez votre version avec `python3 --version` ; certaines plateformes (macOS sans Homebrew, Ubuntu 22.04) ont `python3` par défaut en 3.10 ou antérieur, vous devrez peut-être installer 3.11+ séparément.
|
||||
|
||||
`--skill` pointe vers le répertoire installé du skill (où se trouve `customize.toml`). Le nom du skill est déduit du basename du répertoire, et le script cherche automatiquement `_bmad/custom/{skill-name}.toml` et `{skill-name}.user.toml`.
|
||||
|
||||
|
|
@ -217,17 +217,17 @@ Exemples d’utilisation :
|
|||
|
||||
```bash
|
||||
# Résoudre le bloc agent complet
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /chemin/absolu/vers/bmad-agent-pm \
|
||||
--key agent
|
||||
|
||||
# Résoudre un seul champ
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /chemin/absolu/vers/bmad-agent-pm \
|
||||
--key agent.icon
|
||||
|
||||
# Dump complet
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /chemin/absolu/vers/bmad-agent-pm
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ The `bmad-customize` skill is a guided authoring helper for the **per-skill agen
|
|||
:::note[Prerequisites]
|
||||
|
||||
- BMad installed in your project (see [How to Install BMad](./install-bmad.md))
|
||||
- Python 3.11+ on your PATH (for the resolver script -- uses stdlib `tomllib`, no `pip install`, no `uv`, no virtualenv)
|
||||
- A way to run the resolver script — BMad is standardizing on `uv` (`uv run`, which provisions Python for you); a plain `python3` 3.11+ on your PATH still works during the transition. The script uses only stdlib `tomllib`, so there's nothing to `pip install`.
|
||||
- A text editor for TOML files
|
||||
:::
|
||||
|
||||
|
|
@ -201,15 +201,15 @@ persistent_facts = [
|
|||
|
||||
## How Resolution Works
|
||||
|
||||
On activation, the agent's SKILL.md runs a shared Python script that does the three-layer merge and returns the resolved block as JSON. The script uses the Python standard library's `tomllib` module (no external dependencies), so plain `python3` is enough:
|
||||
On activation, the agent's SKILL.md runs a shared Python script that does the three-layer merge and returns the resolved block as JSON. The script uses only the Python standard library's `tomllib` module (no external dependencies). BMad is standardizing on `uv run` to invoke these scripts (uv provisions a suitable Python for you); a plain `python3` still works during the transition:
|
||||
|
||||
```bash
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill {skill-root} \
|
||||
--key agent
|
||||
```
|
||||
|
||||
**Requirements**: Python 3.11+ (earlier versions don't include `tomllib`). No `pip install`, no `uv`, no virtualenv. Check with `python3 --version`. Some platforms (macOS without Homebrew, Ubuntu 22.04) default `python3` to 3.10 or earlier, so you may need to install 3.11+ separately.
|
||||
**Requirements**: Python 3.11+ (earlier versions don't include `tomllib`); nothing to `pip install`. Running via `uv run` is the going-forward standard — uv resolves a suitable interpreter for you. If you run it with `python3` directly during the transition, check your version with `python3 --version`: some platforms (macOS without Homebrew, Ubuntu 22.04) default `python3` to 3.10 or earlier, so you may need to install 3.11+ separately.
|
||||
|
||||
`--skill` points at the skill's installed directory (where `customize.toml` lives). The skill name is derived from the directory's basename, and the script looks up `_bmad/custom/{skill-name}.toml` and `{skill-name}.user.toml` automatically.
|
||||
|
||||
|
|
@ -217,17 +217,17 @@ Useful invocations:
|
|||
|
||||
```bash
|
||||
# Resolve the full agent block
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /abs/path/to/bmad-agent-pm \
|
||||
--key agent
|
||||
|
||||
# Resolve a single field
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /abs/path/to/bmad-agent-pm \
|
||||
--key agent.icon
|
||||
|
||||
# Full dump
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /abs/path/to/bmad-agent-pm
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Skill `bmad-customize` là trợ lý tạo cấu hình có hướng dẫn cho **
|
|||
:::note[Điều kiện tiên quyết]
|
||||
|
||||
- BMad đã được cài trong dự án của bạn (xem [Cách cài đặt BMad](./install-bmad.md))
|
||||
- Python 3.11+ có trên PATH của bạn (để chạy resolver; dùng stdlib `tomllib`, không cần `pip install`, `uv` hay virtualenv)
|
||||
- Một cách để chạy resolver script — BMad đang chuẩn hóa sang `uv` (`uv run`, tự cấp Python cho bạn); một `python3` 3.11+ thuần trên PATH vẫn dùng được trong giai đoạn chuyển đổi. Script chỉ dùng stdlib `tomllib`, nên không cần `pip install` gì cả.
|
||||
- Một trình soạn thảo văn bản cho file TOML
|
||||
:::
|
||||
|
||||
|
|
@ -201,15 +201,15 @@ persistent_facts = [
|
|||
|
||||
## Cách quá trình resolve diễn ra
|
||||
|
||||
Khi agent được kích hoạt, `SKILL.md` của nó sẽ gọi một shared Python script để merge ba lớp nói trên và trả về block kết quả ở dạng JSON. Script này dùng `tomllib` của Python stdlib, nên `python3` thuần là đủ:
|
||||
Khi agent được kích hoạt, `SKILL.md` của nó sẽ gọi một shared Python script để merge ba lớp nói trên và trả về block kết quả ở dạng JSON. Script này chỉ dùng `tomllib` của Python stdlib (không có dependency ngoài). BMad đang chuẩn hóa sang `uv run` để chạy các script này (uv tự cấp một bản Python phù hợp cho bạn); một `python3` thuần vẫn dùng được trong giai đoạn chuyển đổi:
|
||||
|
||||
```bash
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill {skill-root} \
|
||||
--key agent
|
||||
```
|
||||
|
||||
**Yêu cầu**: Python 3.11+ vì các phiên bản cũ hơn không có `tomllib`. Không cần `pip install`, không cần `uv`, không cần virtualenv. Bạn có thể kiểm tra bằng `python3 --version`. Trên một số nền tảng, `python3` mặc định vẫn là 3.10 hoặc thấp hơn, nên có thể bạn sẽ phải cài 3.11+ riêng.
|
||||
**Yêu cầu**: Python 3.11+ vì các phiên bản cũ hơn không có `tomllib`; không cần `pip install` gì. Chạy qua `uv run` là chuẩn về sau — uv tự tìm một bản interpreter phù hợp cho bạn. Nếu bạn chạy trực tiếp bằng `python3` trong giai đoạn chuyển đổi, hãy kiểm tra phiên bản bằng `python3 --version`: trên một số nền tảng, `python3` mặc định vẫn là 3.10 hoặc thấp hơn, nên có thể bạn sẽ phải cài 3.11+ riêng.
|
||||
|
||||
`--skill` trỏ vào thư mục skill đã cài, nơi có file `customize.toml`. Tên skill được lấy từ basename của thư mục, sau đó script sẽ tự tìm `_bmad/custom/{skill-name}.toml` và `{skill-name}.user.toml`.
|
||||
|
||||
|
|
@ -217,17 +217,17 @@ Một số lệnh hữu ích:
|
|||
|
||||
```bash
|
||||
# Resolve toàn bộ block agent
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /duong-dan/tuyet-doi/toi/bmad-agent-pm \
|
||||
--key agent
|
||||
|
||||
# Resolve một trường cụ thể
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /duong-dan/tuyet-doi/toi/bmad-agent-pm \
|
||||
--key agent.icon
|
||||
|
||||
# Dump toàn bộ
|
||||
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
uv run {project-root}/_bmad/scripts/resolve_customization.py \
|
||||
--skill /duong-dan/tuyet-doi/toi/bmad-agent-pm
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -207,11 +207,11 @@ def test_unknown_category_style_uses_fallback_glyph():
|
|||
|
||||
def test_shipped_selector_is_in_sync_with_catalog():
|
||||
# foolproofing: if someone edits brain-methods.csv they must regenerate the page.
|
||||
# Regenerate with: python3 brain.py html --out assets/brain-selector.html
|
||||
# Regenerate with: uv run brain.py html --out assets/brain-selector.html
|
||||
asset = brain.DEFAULT_FILE.parent / "brain-selector.html"
|
||||
assert asset.is_file(), "missing assets/brain-selector.html — generate it"
|
||||
expected = brain.html_doc(brain.load(brain.DEFAULT_FILE))
|
||||
assert asset.read_text(encoding="utf-8") == expected, (
|
||||
"assets/brain-selector.html is stale; regenerate: "
|
||||
"python3 brain.py html --out assets/brain-selector.html"
|
||||
"uv run brain.py html --out assets/brain-selector.html"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Exercises the scanner against a synthesized install tree:
|
|||
- malformed TOML (surfaces as an error without aborting)
|
||||
- multiple skills roots (e.g. project-local + user-global mix)
|
||||
|
||||
Run: python3 scripts/tests/test_list_customizable_skills.py
|
||||
Run: uv run scripts/tests/test_list_customizable_skills.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ Reads from four layers (highest priority last):
|
|||
|
||||
Outputs merged JSON to stdout. Errors go to stderr.
|
||||
|
||||
Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
|
||||
no virtualenv — plain `python3` is sufficient.
|
||||
Uses only the Python stdlib (`tomllib`) — no third-party dependencies.
|
||||
BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable
|
||||
interpreter for you); a plain `python3` on PATH still works during the
|
||||
transition. Either runner needs Python 3.11+ for `tomllib`.
|
||||
|
||||
python3 resolve_config.py --project-root /abs/path/to/project
|
||||
python3 resolve_config.py --project-root ... --key core
|
||||
python3 resolve_config.py --project-root ... --key agents
|
||||
uv run resolve_config.py --project-root /abs/path/to/project
|
||||
uv run resolve_config.py --project-root ... --key core
|
||||
uv run resolve_config.py --project-root ... --key agents
|
||||
|
||||
Merge rules (same as resolve_customization.py):
|
||||
- Scalars: override wins
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ Skill name is derived from the basename of the skill directory.
|
|||
|
||||
Outputs merged JSON to stdout. Errors go to stderr.
|
||||
|
||||
Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
|
||||
no virtualenv — plain `python3` is sufficient.
|
||||
Uses only the Python stdlib (`tomllib`) — no third-party dependencies.
|
||||
BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable
|
||||
interpreter for you); a plain `python3` on PATH still works during the
|
||||
transition. Either runner needs Python 3.11+ for `tomllib`.
|
||||
|
||||
python3 resolve_customization.py --skill /abs/path/to/skill-dir
|
||||
python3 resolve_customization.py --skill ... --key agent
|
||||
python3 resolve_customization.py --skill ... --key agent.menu
|
||||
uv run resolve_customization.py --skill /abs/path/to/skill-dir
|
||||
uv run resolve_customization.py --skill ... --key agent
|
||||
uv run resolve_customization.py --skill ... --key agent.menu
|
||||
|
||||
Merge rules (purely structural — no field-name special-casing):
|
||||
- Scalars (string, int, bool, float): override wins
|
||||
|
|
|
|||
|
|
@ -3319,134 +3319,63 @@ async function runTests() {
|
|||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 46: Python environment check (version parsing + classification)
|
||||
// Test Suite 46: uv environment check (version parsing + messaging)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 46: python-check version parsing and classification${colors.reset}\n`);
|
||||
console.log(`${colors.yellow}Test Suite 46: uv-check version parsing and messaging${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
const { parsePythonVersion, classifyPython, detectPython } = require('../tools/installer/core/python-check');
|
||||
const { parseUvVersion, detectUv } = require('../tools/installer/core/uv-check');
|
||||
|
||||
// Version parsing
|
||||
const v312 = parsePythonVersion('Python 3.12.1');
|
||||
assert(v312 && v312.major === 3 && v312.minor === 12 && v312.patch === 1, 'parses "Python 3.12.1"');
|
||||
const v311 = parsePythonVersion('Python 3.11.0\n');
|
||||
assert(v311 && v311.raw === '3.11.0', 'parses with trailing newline');
|
||||
const v2 = parsePythonVersion('\nPython 2.7.18');
|
||||
assert(v2 && v2.major === 2, 'parses Python 2 output (stderr-style)');
|
||||
const noPatch = parsePythonVersion('Python 3.13');
|
||||
const plain = parseUvVersion('uv 0.5.31');
|
||||
assert(plain && plain.major === 0 && plain.minor === 5 && plain.patch === 31, 'parses "uv 0.5.31"');
|
||||
const brew = parseUvVersion('uv 0.5.31 (Homebrew 2025-02-12)');
|
||||
assert(brew && brew.raw === '0.5.31', 'parses uv version with build suffix');
|
||||
const noPatch = parseUvVersion('uv 1.2');
|
||||
assert(noPatch && noPatch.patch === 0, 'missing patch defaults to 0');
|
||||
assert(parsePythonVersion('') === null, 'empty output returns null');
|
||||
assert(parsePythonVersion('command not found: python3') === null, 'non-version output returns null');
|
||||
assert(parsePythonVersion(null) === null, 'null output returns null');
|
||||
assert(parseUvVersion('') === null, 'empty output returns null');
|
||||
assert(parseUvVersion('command not found: uv') === null, 'non-version output returns null');
|
||||
assert(parseUvVersion(null) === null, 'null output returns null');
|
||||
|
||||
// Classification against feature requirements
|
||||
assert(classifyPython({ major: 3, minor: 11 }) === 'full', '3.11 is full support (tomllib floor)');
|
||||
assert(classifyPython({ major: 3, minor: 13 }) === 'full', '3.13 is full support');
|
||||
assert(classifyPython({ major: 4, minor: 0 }) === 'full', 'hypothetical 4.0 is full support');
|
||||
assert(classifyPython({ major: 3, minor: 10 }) === 'partial', '3.10 is partial (memlog yes, tomllib no)');
|
||||
assert(classifyPython({ major: 3, minor: 8 }) === 'partial', '3.8 is partial (memlog floor)');
|
||||
assert(classifyPython({ major: 3, minor: 7 }) === 'unsupported', '3.7 is unsupported');
|
||||
assert(classifyPython({ major: 2, minor: 7 }) === 'unsupported', '2.7 is unsupported');
|
||||
assert(classifyPython(null) === 'none', 'no python is none');
|
||||
// Detection smoke test — must not throw; result is null or well-formed.
|
||||
const detectedUv = detectUv();
|
||||
assert(detectedUv === null || typeof detectedUv.version.raw === 'string', 'detectUv returns null or a well-formed result');
|
||||
|
||||
// Detection smoke test — must not throw, and if it finds a Python the
|
||||
// result must be well-formed. (CI machines may or may not have Python.)
|
||||
const detected = detectPython();
|
||||
assert(
|
||||
detected === null ||
|
||||
(typeof detected.command === 'string' &&
|
||||
typeof detected.version.raw === 'string' &&
|
||||
typeof detected.isRuntimeCommand === 'boolean'),
|
||||
'detectPython returns null or a well-formed result',
|
||||
);
|
||||
|
||||
// checkPythonEnvironment branch coverage — stub detection, prompts, and
|
||||
// process.exit so the assertions are deterministic regardless of the
|
||||
// machine's Python. python-check resolves detectPython via module.exports
|
||||
// and prompts via the shared module object, so swapping properties works.
|
||||
const pythonCheck = require('../tools/installer/core/python-check');
|
||||
// checkUvEnvironment branch coverage — stub detection + prompts so the
|
||||
// assertions are deterministic regardless of whether uv is installed.
|
||||
const uvCheck = require('../tools/installer/core/uv-check');
|
||||
const promptsModule = require('../tools/installer/prompts');
|
||||
const real = {
|
||||
detectPython: pythonCheck.detectPython,
|
||||
log: promptsModule.log,
|
||||
note: promptsModule.note,
|
||||
select: promptsModule.select,
|
||||
cancel: promptsModule.cancel,
|
||||
exit: process.exit,
|
||||
};
|
||||
const stub = (detectResult, selectAnswer) => {
|
||||
const seen = { success: [], warn: [], info: [], note: [], select: [], cancel: [], exit: [] };
|
||||
pythonCheck.detectPython = () => detectResult;
|
||||
const realUv = { detectUv: uvCheck.detectUv, log: promptsModule.log, note: promptsModule.note };
|
||||
const stubUv = (detectResult) => {
|
||||
const seen = { success: [], warn: [], note: [] };
|
||||
uvCheck.detectUv = () => detectResult;
|
||||
promptsModule.log = {
|
||||
success: async (m) => void seen.success.push(m),
|
||||
warn: async (m) => void seen.warn.push(m),
|
||||
info: async (m) => void seen.info.push(m),
|
||||
info: async () => {},
|
||||
error: async () => {},
|
||||
};
|
||||
promptsModule.note = async (m, t) => void seen.note.push(t || m);
|
||||
promptsModule.select = async (opts) => {
|
||||
seen.select.push(opts.message);
|
||||
return selectAnswer;
|
||||
};
|
||||
promptsModule.cancel = async (m) => void seen.cancel.push(m);
|
||||
process.exit = (code) => {
|
||||
seen.exit.push(code);
|
||||
throw new Error('__stub_exit__');
|
||||
};
|
||||
return seen;
|
||||
};
|
||||
|
||||
try {
|
||||
const v = (major, minor, patch) => ({ major, minor, patch, raw: `${major}.${minor}.${patch}` });
|
||||
// Branch: uv present — success, no warning.
|
||||
let seen = stubUv({ version: { major: 0, minor: 5, patch: 31, raw: '0.5.31' } });
|
||||
let result = await uvCheck.checkUvEnvironment();
|
||||
assert(result.status === 'found' && seen.success.length === 1, 'uv present logs success');
|
||||
assert(seen.success[0].includes('uv run') && seen.warn.length === 0, 'uv present mentions uv run, no warning');
|
||||
|
||||
// Branch: full support via the runtime command — success, no prompt.
|
||||
let seen = stub({ command: 'python3', version: v(3, 12, 1), isRuntimeCommand: true }, 'continue');
|
||||
let result = await pythonCheck.checkPythonEnvironment();
|
||||
assert(result.status === 'full' && seen.success.length === 1, 'full support via python3 logs success');
|
||||
assert(seen.select.length === 0 && seen.warn.length === 0, 'full support via python3 skips warning and ack prompt');
|
||||
|
||||
// Branch: modern Python found, but not as `python3` — runtime mismatch.
|
||||
seen = stub({ command: 'py -3', version: v(3, 12, 0), isRuntimeCommand: false }, 'continue');
|
||||
result = await pythonCheck.checkPythonEnvironment();
|
||||
assert(seen.success.length === 0, 'python3-mismatch never reports full support');
|
||||
assert(
|
||||
seen.warn.length === 1 && seen.warn[0].includes('python3') && seen.warn[0].includes('py -3'),
|
||||
'python3-mismatch warns that scripts invoke python3',
|
||||
);
|
||||
assert(seen.select.length === 1 && result.status === 'full', 'python3-mismatch still requires the ack prompt');
|
||||
|
||||
// Branch: partial support (3.8–3.10) — warn + ack, continue returns.
|
||||
seen = stub({ command: 'python3', version: v(3, 9, 5), isRuntimeCommand: true }, 'continue');
|
||||
result = await pythonCheck.checkPythonEnvironment();
|
||||
assert(
|
||||
result.status === 'partial' && seen.warn.length === 1 && seen.warn[0].includes('3.11+'),
|
||||
'partial support warns about tomllib floor',
|
||||
);
|
||||
assert(seen.select.length === 1 && seen.exit.length === 0, 'partial support prompts and continue proceeds');
|
||||
|
||||
// Branch: no Python, non-interactive — warn + info, never prompts.
|
||||
seen = stub(null, 'continue');
|
||||
result = await pythonCheck.checkPythonEnvironment({ nonInteractive: true });
|
||||
assert(result.status === 'none' && seen.warn[0].includes('No Python found'), 'non-interactive with no Python warns');
|
||||
assert(seen.select.length === 0 && seen.info.length === 1, 'non-interactive skips the ack prompt and logs continuation');
|
||||
|
||||
// Branch: no Python, interactive, user quits — cancel message + exit 0.
|
||||
seen = stub(null, 'quit');
|
||||
let threw = false;
|
||||
try {
|
||||
await pythonCheck.checkPythonEnvironment();
|
||||
} catch (error) {
|
||||
threw = error.message === '__stub_exit__';
|
||||
}
|
||||
assert(threw && seen.exit.length === 1 && seen.exit[0] === 0, 'quit choice exits 0 (user-cancel convention)');
|
||||
assert(seen.cancel.length === 1, 'quit choice shows the cancel guidance');
|
||||
// Branch: uv missing — warn + setup note, never blocks (no prompt).
|
||||
seen = stubUv(null);
|
||||
result = await uvCheck.checkUvEnvironment();
|
||||
assert(result.status === 'missing' && seen.warn.length === 1, 'uv missing warns');
|
||||
assert(seen.warn[0].includes('de facto standard'), 'uv-missing warning frames uv as the de facto standard');
|
||||
assert(seen.note.length === 1 && seen.note[0].includes('uv'), 'uv missing shows a setup note');
|
||||
} finally {
|
||||
pythonCheck.detectPython = real.detectPython;
|
||||
promptsModule.log = real.log;
|
||||
promptsModule.note = real.note;
|
||||
promptsModule.select = real.select;
|
||||
promptsModule.cancel = real.cancel;
|
||||
process.exit = real.exit;
|
||||
uvCheck.detectUv = realUv.detectUv;
|
||||
promptsModule.log = realUv.log;
|
||||
promptsModule.note = realUv.note;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 46 setup failed: ${error.message}${colors.reset}`);
|
||||
|
|
|
|||
|
|
@ -1233,6 +1233,9 @@ class Installer {
|
|||
` 1. Launch your AI agent from your project folder`,
|
||||
` 2. Not sure what to do? Invoke the ${color.cyan('bmad-help')} skill and ask it what to do!`,
|
||||
'',
|
||||
` ${color.cyan('Tip:')} BMAD workflows increasingly run Python scripts via ${color.cyan('uv run')} — uv is`,
|
||||
` becoming the de facto standard. If you don't have it yet, ask your agent to set it up.`,
|
||||
'',
|
||||
` Blog, Docs and Guides: ${color.blue('https://bmadcode.com/')}`,
|
||||
` Community: ${color.blue('https://discord.gg/gk8jAdXWmj')}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
const { spawnSync } = require('node:child_process');
|
||||
const prompts = require('../prompts');
|
||||
|
||||
// Python 3.11 added stdlib `tomllib` (PEP 680), which the shared scripts in
|
||||
// src/scripts/ (resolve_config.py, resolve_customization.py) require to read
|
||||
// BMAD's TOML config files. memlog.py is more lenient and runs on 3.8+.
|
||||
const PYTHON_FULL_SUPPORT = { major: 3, minor: 11 };
|
||||
const PYTHON_PARTIAL_SUPPORT = { major: 3, minor: 8 };
|
||||
|
||||
// Every runtime call site (skill steps, on_complete hooks) invokes a literal
|
||||
// `python3`, so only that command's version vouches for BMAD features. The
|
||||
// fallback probes exist to tell the user "Python is installed, but not under
|
||||
// the name BMAD uses" instead of a misleading "No Python found".
|
||||
const RUNTIME_COMMAND = 'python3';
|
||||
const PROBE_CANDIDATES =
|
||||
process.platform === 'win32'
|
||||
? [
|
||||
{ command: 'python3', args: ['--version'] },
|
||||
{ command: 'py', args: ['-3', '--version'] },
|
||||
{ command: 'python', args: ['--version'] },
|
||||
]
|
||||
: [
|
||||
{ command: 'python3', args: ['--version'] },
|
||||
{ command: 'python', args: ['--version'] },
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse a `python --version` output line into version parts.
|
||||
* Python 3 prints to stdout; Python 2 printed to stderr — callers pass both.
|
||||
* @param {string} output - Combined stdout/stderr from `python --version`
|
||||
* @returns {{major: number, minor: number, patch: number, raw: string}|null}
|
||||
*/
|
||||
function parsePythonVersion(output) {
|
||||
if (!output) return null;
|
||||
const match = output.match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3] || 0),
|
||||
raw: `${match[1]}.${match[2]}.${match[3] || 0}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a detected Python version against BMAD's feature requirements.
|
||||
* @param {{major: number, minor: number}|null} version
|
||||
* @returns {'full'|'partial'|'unsupported'|'none'}
|
||||
*/
|
||||
function classifyPython(version) {
|
||||
if (!version) return 'none';
|
||||
const { major, minor } = version;
|
||||
if (major > PYTHON_FULL_SUPPORT.major || (major === PYTHON_FULL_SUPPORT.major && minor >= PYTHON_FULL_SUPPORT.minor)) {
|
||||
return 'full';
|
||||
}
|
||||
if (major === PYTHON_PARTIAL_SUPPORT.major && minor >= PYTHON_PARTIAL_SUPPORT.minor) {
|
||||
return 'partial';
|
||||
}
|
||||
return 'unsupported';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one probe candidate and return its parsed version, or null.
|
||||
* @param {{command: string, args: string[]}} candidate
|
||||
* @returns {{major: number, minor: number, patch: number, raw: string}|null}
|
||||
*/
|
||||
function probeVersion(candidate) {
|
||||
const run = (extra = {}) =>
|
||||
spawnSync(candidate.command, candidate.args, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
...extra,
|
||||
});
|
||||
let result = run();
|
||||
// Node >=18.20/20.12 refuses to spawn .bat/.cmd without a shell
|
||||
// (CVE-2024-27980 hardening) and reports EINVAL — pyenv-win ships its
|
||||
// python shims as .bat. Args here are static literals, so a shell retry
|
||||
// is injection-safe.
|
||||
if (result.error && result.error.code === 'EINVAL' && process.platform === 'win32') {
|
||||
result = run({ shell: true });
|
||||
}
|
||||
if (result.error) return null;
|
||||
return parsePythonVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe the local environment for a Python interpreter.
|
||||
* Tries each candidate command and returns the first that reports a version.
|
||||
* `isRuntimeCommand` is true only when the match is `python3` — the command
|
||||
* BMAD scripts actually invoke.
|
||||
* @returns {{command: string, version: {major: number, minor: number, patch: number, raw: string}, isRuntimeCommand: boolean}|null}
|
||||
*/
|
||||
function detectPython() {
|
||||
for (const candidate of PROBE_CANDIDATES) {
|
||||
try {
|
||||
const version = probeVersion(candidate);
|
||||
if (version) {
|
||||
const display = candidate.args.length > 1 ? `${candidate.command} ${candidate.args.slice(0, -1).join(' ')}` : candidate.command;
|
||||
return { command: display, version, isRuntimeCommand: candidate.command === RUNTIME_COMMAND };
|
||||
}
|
||||
} catch {
|
||||
// Candidate not runnable — try the next one.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function upgradeHints() {
|
||||
return [
|
||||
'How to get Python 3.11+ (as `python3`):',
|
||||
' macOS: brew install python3',
|
||||
' Windows: winget install Python.Python.3.12 (then ensure `python3` resolves, e.g. enable the python3 alias)',
|
||||
' Linux/WSL: sudo apt install python3 (Ubuntu 24.04+ ships 3.12; older distros: use pyenv or deadsnakes)',
|
||||
' Docker: add python3 to your image (e.g. apk add python3 / apt-get install -y python3)',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the local Python environment and warn about degraded BMAD features.
|
||||
*
|
||||
* Warn-don't-block: most of BMAD works without Python, so the install always
|
||||
* may proceed — but the user must explicitly acknowledge the warning so it
|
||||
* can't scroll past unseen. In non-interactive runs (--yes, or stdin is not
|
||||
* a TTY) the warning is logged and the install continues without a prompt.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.nonInteractive=false] - Skip the ack prompt (--yes, or no TTY)
|
||||
* @returns {Promise<{status: string, detected: Object|null}>}
|
||||
*/
|
||||
async function checkPythonEnvironment({ nonInteractive = false } = {}) {
|
||||
// Called via module.exports so tests can stub detection.
|
||||
const detected = module.exports.detectPython();
|
||||
const status = classifyPython(detected ? detected.version : null);
|
||||
|
||||
if (status === 'full' && detected.isRuntimeCommand) {
|
||||
await prompts.log.success(`Python ${detected.version.raw} detected (${detected.command}) — all BMAD features supported.`);
|
||||
return { status, detected };
|
||||
}
|
||||
|
||||
if (detected && !detected.isRuntimeCommand) {
|
||||
await prompts.log.warn(
|
||||
`Python ${detected.version.raw} found via \`${detected.command}\`, but BMAD scripts invoke \`python3\`, which is not on PATH.\n` +
|
||||
`Python-powered features (memlog session memory, TOML config resolution) won't run until \`python3\` resolves —\n` +
|
||||
`add a python3 alias/shim, or reinstall Python with the python3 launcher enabled.`,
|
||||
);
|
||||
} else if (status === 'partial') {
|
||||
await prompts.log.warn(
|
||||
`Python ${detected.version.raw} detected (${detected.command}) — BMAD's TOML config tools need Python 3.11+ (stdlib tomllib).\n` +
|
||||
`Works: memlog session memory. Won't work: config/customization resolution scripts.`,
|
||||
);
|
||||
} else {
|
||||
const found =
|
||||
status === 'unsupported' ? `Python ${detected.version.raw} detected (${detected.command}) — too old.` : 'No Python found on PATH.';
|
||||
await prompts.log.warn(
|
||||
`${found} BMAD installs fine without it, but Python-powered features\n` +
|
||||
`(memlog session memory, TOML config resolution) won't run until Python 3.11+ is available.`,
|
||||
);
|
||||
}
|
||||
await prompts.note(upgradeHints(), 'Python 3.11+ recommended');
|
||||
|
||||
if (nonInteractive) {
|
||||
await prompts.log.info('Continuing anyway (non-interactive run). You can fix Python later — no reinstall needed.');
|
||||
return { status, detected };
|
||||
}
|
||||
|
||||
const choice = await prompts.select({
|
||||
message: "BMAD's Python-powered features won't work yet. How do you want to proceed?",
|
||||
choices: [
|
||||
{
|
||||
name: 'Continue install',
|
||||
value: 'continue',
|
||||
hint: 'BMAD works without Python — you can fix Python later, no reinstall needed',
|
||||
},
|
||||
{
|
||||
name: 'Quit and fix Python first',
|
||||
value: 'quit',
|
||||
hint: 'make Python 3.11+ available as python3, then re-run the installer',
|
||||
},
|
||||
],
|
||||
default: 'continue',
|
||||
});
|
||||
|
||||
if (choice === 'quit') {
|
||||
await prompts.cancel('Make Python 3.11+ available as `python3` (see hints above), then re-run the installer.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return { status, detected };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkPythonEnvironment,
|
||||
detectPython,
|
||||
parsePythonVersion,
|
||||
classifyPython,
|
||||
PYTHON_FULL_SUPPORT,
|
||||
PYTHON_PARTIAL_SUPPORT,
|
||||
};
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
const { spawnSync } = require('node:child_process');
|
||||
const prompts = require('../prompts');
|
||||
|
||||
// `uv` (https://docs.astral.sh/uv/) is becoming the de facto standard for
|
||||
// running the Python scripts BMAD workflows shell out to: `uv run <script>`
|
||||
// resolves the interpreter and any dependencies on demand, so skills don't
|
||||
// have to assume a particular `python3` is on PATH. The ecosystem is mid-
|
||||
// migration — some skills still call `python3` directly — so a missing `uv`
|
||||
// is a warning, not a blocker: BMAD installs and runs either way.
|
||||
const RUNTIME_COMMAND = 'uv';
|
||||
|
||||
/**
|
||||
* Parse `uv --version` output into version parts.
|
||||
* Example outputs: "uv 0.5.31", "uv 0.5.31 (Homebrew 2025-02-12)".
|
||||
* @param {string} output - stdout/stderr from `uv --version`
|
||||
* @returns {{major: number, minor: number, patch: number, raw: string}|null}
|
||||
*/
|
||||
function parseUvVersion(output) {
|
||||
if (!output) return null;
|
||||
const match = output.match(/uv\s+(\d+)\.(\d+)(?:\.(\d+))?/i);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3] || 0),
|
||||
raw: `${match[1]}.${match[2]}.${match[3] || 0}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe the local environment for `uv`.
|
||||
* @returns {{version: {major: number, minor: number, patch: number, raw: string}}|null}
|
||||
*/
|
||||
function detectUv() {
|
||||
let result;
|
||||
try {
|
||||
result = spawnSync(RUNTIME_COMMAND, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!result || result.error) return null;
|
||||
const version = parseUvVersion(`${result.stdout || ''}\n${result.stderr || ''}`);
|
||||
return version ? { version } : null;
|
||||
}
|
||||
|
||||
function setupHints() {
|
||||
return [
|
||||
'BMAD workflows increasingly run Python scripts via `uv run`, which manages',
|
||||
'the interpreter and dependencies for you — no manual venv or pip needed.',
|
||||
'',
|
||||
'Easiest path: ask your AI agent to "install and set up uv for me".',
|
||||
'',
|
||||
'Or install it yourself:',
|
||||
' macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh',
|
||||
' Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"',
|
||||
' Homebrew: brew install uv',
|
||||
' Docs: https://docs.astral.sh/uv/getting-started/installation/',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether `uv` is available and inform the user.
|
||||
*
|
||||
* Warn-don't-block, and no acknowledgement prompt: `uv` is on its way to being
|
||||
* the standard runner for BMAD's Python scripts, but the migration is still in
|
||||
* progress, so the install never stops on its account. The note tells the user
|
||||
* how to set it up (preferably by asking their agent).
|
||||
*
|
||||
* @returns {Promise<{status: 'found'|'missing', detected: Object|null}>}
|
||||
*/
|
||||
async function checkUvEnvironment() {
|
||||
// Called via module.exports so tests can stub detection.
|
||||
const detected = module.exports.detectUv();
|
||||
|
||||
if (detected) {
|
||||
await prompts.log.success(`uv ${detected.version.raw} detected — ready to run BMAD's Python-powered scripts via \`uv run\`.`);
|
||||
return { status: 'found', detected };
|
||||
}
|
||||
|
||||
await prompts.log.warn(
|
||||
"uv not found on PATH. uv is becoming the de facto standard for running BMAD's Python\n" +
|
||||
'scripts (`uv run <script>`), and it provisions the interpreter for you. BMAD installs\n' +
|
||||
'fine without it, but setting up uv now keeps you ahead as workflows adopt it.',
|
||||
);
|
||||
await prompts.note(setupHints(), 'uv recommended');
|
||||
return { status: 'missing', detected: null };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkUvEnvironment,
|
||||
detectUv,
|
||||
parseUvVersion,
|
||||
};
|
||||
|
|
@ -12,6 +12,10 @@ startMessage: |
|
|||
🌟 100% free. 100% open source. Always.
|
||||
No paywalls. No gated content. Knowledge shared, not sold.
|
||||
|
||||
🐍 HEADS UP: uv (https://docs.astral.sh/uv/) is becoming the de facto standard
|
||||
for running the Python scripts BMAD workflows rely on (`uv run <script>`).
|
||||
If it's not set up yet, ask your AI agent to "install and set up uv for me".
|
||||
|
||||
🌐 CONNECT:
|
||||
Website: https://bmadcode.com/
|
||||
Discord: https://discord.gg/gk8jAdXWmj
|
||||
|
|
|
|||
|
|
@ -161,15 +161,16 @@ class UI {
|
|||
const messageLoader = new MessageLoader();
|
||||
await messageLoader.displayStartMessage();
|
||||
|
||||
// Probe the local Python before any other prompts: several BMAD features
|
||||
// (memlog session memory, TOML config resolution) need Python 3.11+ at
|
||||
// runtime. Warn-don't-block, but require an explicit ack so the warning
|
||||
// can't scroll past unseen. The installer runs in the destination
|
||||
// environment, so probing PATH here tests the right machine.
|
||||
// Skip the ack when stdin isn't a TTY (CI/Docker/piped): clack's select
|
||||
// on closed stdin resolves to cancel, which would silently exit 0.
|
||||
const { checkPythonEnvironment } = require('./core/python-check');
|
||||
await checkPythonEnvironment({ nonInteractive: !!options.yes || !process.stdin.isTTY });
|
||||
// Probe for `uv` before any other prompts: it's becoming the de facto
|
||||
// runner for the Python scripts BMAD workflows shell out to
|
||||
// (`uv run <script>`), and uv provisions the interpreter itself, so it's
|
||||
// the single thing worth checking for. The migration is still in progress
|
||||
// (some skills still call `python3` directly), so this is informational —
|
||||
// warn-don't-block, no ack prompt — and just points the user at setup
|
||||
// (ideally "ask your agent to set up uv"). The installer runs in the
|
||||
// destination environment, so probing PATH here tests the right machine.
|
||||
const { checkUvEnvironment } = require('./core/uv-check');
|
||||
await checkUvEnvironment();
|
||||
|
||||
// Parse channel flags (--channel/--all-*/--next=/--pin) once. Warnings
|
||||
// are surfaced immediately so the user sees them before any git ops run.
|
||||
|
|
|
|||
Loading…
Reference in New Issue